Skip to content

Infrastructure & Security

How I Manage My Self-Hosted Web Infrastructure Securely

A practical documentation of the current shape of my self-hosted setup and the security decisions behind splitting web hosting, app deployment, and management access.

I run a small self-hosted infrastructure for my personal websites, educational tools, and experimental web applications. The goal is simple: I want to be able to publish websites and web apps quickly, while keeping the system maintainable enough that I can patch it, audit it, and recover it without turning every deployment into a one-off server project.

This post documents the current shape of that infrastructure and the security decisions behind it. It is not a claim that the setup is perfect. It is a working model: practical, relatively small, and designed around clear boundaries.


The Infrastructure at a Glance

The setup is split into two main server roles to reduce operational coupling and simplify maintenance.

Public Web Hosting

Hestia / WordPress Server

Runs Ubuntu on Oracle Cloud with Hestia Control Panel. Hosts public websites and static or browser-based calculators.

  • ServicesWordPress Sites, static browser tools
  • DatabaseMariaDB (Local connections only)
  • BackendNginx reverse proxy + Apache backend
  • Public Ports80 (HTTP), 443 (HTTPS)
  • Admin AccessSSH & Hestia panel (Tailscale only)

App Deployments

Coolify / Docker Server

Runs Coolify to manage containerized web applications, standalone tools, and automations without manual stack management.

  • ServicesAeroLab, Weibull Analyzer, OpenClaw, Hermes
  • RuntimeDocker containers, node/nginx stacks
  • StorageApp-specific persistent volumes
  • Public Ports80/443 for public app domains
  • Admin AccessCoolify Panel & SSH (Tailscale only)

Both servers are connected to my Tailscale tailnet for secure private administration. Public web traffic is strictly limited to ports 80 and 443 for visitors.

The important distinction is that WordPress pages and standalone apps are related from a content point of view, but not necessarily hosted in the same way. For example, the WordPress page at https://dwiwidyanto.com/tools/aerolab/ can introduce and link to AeroLab, while the actual AeroLab app is deployed separately at https://aerolab.dwiwidyanto.com/ through Coolify.


Why I Split Web Hosting and App Deployment

WordPress hosting and containerized app hosting have different operational needs.

For WordPress and traditional websites, Hestia is convenient. It handles common hosting tasks cleanly: domains, SSL certificates, web users, web roots, backups, and PHP-FPM integration. It is a good fit for sites like dwiwidyanto.com, vikhacraft.com, diaspot.com, and small browser tools embedded into WordPress.

For custom web apps and long-running services, Coolify is a better fit. It lets me deploy containers, manage environment variables, restart apps, inspect deployment logs, and keep app state in explicit volumes. It also makes it easier to run experimental tools without polluting the WordPress server.

This separation reduces operational coupling. If I break an experimental app container, I do not want that to affect the public WordPress server. If I update WordPress or PHP on the Hestia server, I do not want that to disturb unrelated app services.


Public Surface Area: Keep It Small

The first security rule is to keep the public surface area small. On the Hestia server, the public internet only needs access to ports 80 (HTTP) and 443 (HTTPS).

Administrative panels and secure shell access are completely restricted to the private tailnet:

  • 80/tcpHTTP (Public Internet)
  • 443/tcpHTTPS (Public Internet)
  • 22/tcpSSH / SFTP (Tailscale Tailnet Only)
  • 2083/tcpHestia Control Panel (Tailscale Tailnet Only)

Mail, DNS, FTP, POP3, and IMAP were disabled because I do not currently use this server for those roles. Leaving unused services running creates unnecessary patching and monitoring work. If a service has no job, it should not be listening.

This also means I use SFTP instead of FTP. SFTP runs over SSH, uses the same key-based authentication, and avoids reopening legacy FTP ports and passive FTP ranges.

The Coolify server also exposes public HTTP/HTTPS only for the app domains that are meant to be public. The Coolify administrative interface and SSH access should remain private management surfaces, not general public endpoints.


Tailscale as the Management Network

Tailscale is useful because it lets me avoid exposing administration interfaces to the public internet.

For the Hestia server, the firewall policy is shaped like this:

  • 80, 443Allowed from 0.0.0.0/0 (Everyone)
  • 22 (SSH)Allowed from 100.64.0.0/10 (Tailscale Tailnet Only)
  • 2083 (Hestia)Allowed from 100.64.0.0/10 (Tailscale Tailnet Only)

The 100.64.0.0/10 range is the Tailscale address range. That allows devices in my tailnet to reach SSH and Hestia, while keeping those ports closed to the public internet.

This is a major improvement, but it does not mean every tailnet device should be trusted equally. A private network is not automatically a safe network. If an app server or containerized agent can reach every tailnet device, then a compromise on that app server can become a lateral movement problem.

For that reason, the next layer is access control: home devices can administer servers, but app containers should not automatically be able to reach home devices or management panels.


Docker and Coolify: Useful Isolation, Not a Security Boundary

Coolify and Docker help a lot, but they do not remove the need for network and secret discipline. For containerized applications and automation services, I check the important container properties:

  • Privileged ModeSet to False (Restricts root access)
  • Docker SocketNo Socket Mounts (Prevents Docker host control)
  • Network ModeNo Host Network Mode (Forces bridge/isolated networks)
  • Filesystem MountsApp-specific directories only

The most important thing to avoid is giving ordinary application containers access to the Docker control plane. A container with Docker socket access can usually control the host through Docker. Platform helper containers may need that kind of access, but normal app and automation containers should not have it.

The remaining concern is outbound network access. When a private management network is present on the host, containers may be able to reach private addresses through Docker NAT unless egress is restricted. In testing, I confirmed this general risk and treated it as something to block by default rather than something to rely on application behavior to avoid.

The mitigation is to block Docker containers from initiating connections to private management-network ranges unless they explicitly need that access. The exact firewall rule depends on the host and network design, but the important idea is that this belongs on the Docker host, not inside an individual container. It should restrict container-to-management-network traffic while still allowing the host itself to use the private management network.


GitHub as the Revision and Deployment Source

I use GitHub as the source of truth for custom apps. This gives each app a visible change history, an easy rollback target, and a clean handoff point for deployment. AeroLab and Weibull analyzer are versioned in public repositories:

AeroLab has been developed through small commits rather than untracked server edits. Recent commits include:

08c6f96  Build AeroLab MVP
97f9eff  Add sharing localization and exports
15336ef  Add interactive AeroLab learning modes
02791fd  Update AeroLab home link

The Weibull analyzer follows the same pattern. Recent commits include:

3852735  Initial Weibull analyzer app
9d09537  Add Weibull learning panels
afac6bc  Render Weibull powers as superscripts
268a9fd  Add home link button
0fa49b4  Add B10 median life and banner guidance

That history matters operationally. When a change is deployed, Coolify records the Git commit SHA it pulled from GitHub. If something breaks, the question is not “what did I change on the server?” but “which commit is currently deployed?”

This is the key deployment workflow for standalone apps:

Local development → git commit → git push to GitHub → Coolify pulls the repository → Docker image is built or reused → rolling container update → public app domain updates

For AeroLab, Coolify is connected to the dwiwidyanto/Aerolab repository on the master branch. The Weibull Reliability Analyzer is connected to the dwiwidyanto/weibull_new repository on the main branch. Both apps use Dockerfile-based deployment. The Dockerfile pattern builds the React/Vite app in a Node stage and serves the static output through nginx.

That gives me a useful operational boundary: the app is reproducible from the repository, not from manual files copied onto a server.


AeroLab & Weibull: Standalone App Case Studies

AeroLab is a good example of when I prefer Coolify over embedding everything inside WordPress. AeroLab is an educational aeronautical engineering simulator located at aerolab.dwiwidyanto.com and linked directly from the WordPress tools section. Built with React, TypeScript, Vite, Tailwind CSS, Recharts, and served as static files by Nginx inside a Docker container, it runs entirely client-side. It has no login, no database, and no backend, which makes it safe and highly stable.

Similarly, the Weibull Reliability Analyzer was originally published as a browser-based tool inside WordPress. That made ongoing development harder, so it was moved to its own repository and deployed through Coolify at weibull.dwiwidyanto.com. Cloudflare DNS points the CNAME record to Coolify, while WordPress remains the publishing layer.

The Weibull app includes failure data editors, settings, and charts for reliability curves, hazard levels, and mathematical proofs. Shape β, scale η, MTTF, and B10 metrics help engineers understand reliability curves easily. It links directly back to the WordPress home page via the header.

Some smaller, less complex tools can still be hosted directly inside WordPress when they have no build pipeline. However, for applications requiring independent lifecycle iterations, structured testing, or API secrets, a containerized setup on Coolify is the clear choice.


Secrets and Mounted Volumes

For agent-like services and background integrations, credentials and secrets represent the highest security value. Anything mounted into a container (e.g., config directories, database volumes, browser session profiles, local caches) is accessible to the process running inside it. Mounting read-write files should be managed with high constraint.

The rule is simple: supply only the configuration files, tokens, and storage permissions a container needs to execute its immediate task. The following asset classifications demand strict auditing:

environment files authentication tokens OAuth refresh states browser profiles local databases session logs workspace files

Repositories should store code and environment overrides, never production values, certificates, or deployment webhook keys. Secrets belong in the hosting server configuration or Coolify environment management variables, completely isolated from client bundles and public revision controls.


WordPress Hardening

WordPress is the primary public surface of the website, which makes it a key target. The WordPress instance is hardened using a multi-step checklist:

  • Service DisablingUnused mail, DNS, FTP, POP3, and IMAP services were disabled.
  • Access IsolationHestia control panel and SSH access were moved behind Tailscale.
  • File LockdownsTheme/plugin editor disabled via DISALLOW_FILE_EDIT.
  • SSH HardeningTightened authorized_keys file permissions.
  • Package IntegrityRestored Hestia repositories and cleaned straggling Apache packages.

The DISALLOW_FILE_EDIT setting is small but useful: define('DISALLOW_FILE_EDIT', true);. It prevents theme and plugin editing from the WordPress dashboard. That does not stop all attacks, but it removes one convenient path for turning a compromised WordPress admin session into direct code modification.

Security is not only about blocking attacks. It is also about being able to patch and recover. For the Hestia server, I keep local Hestia backups and make configuration backups before package changes. For app deployments, GitHub is part of the recovery story. If a container deployment fails, I can inspect the commit history, rebuild from a commit, or revert and redeploy.

Patchability is part of security. A system that cannot be updated cleanly becomes fragile.


The Operating Principles

My web infrastructure strategy is guided by ten strict operating principles:

  1. Expose only what must be public. Public websites and app domains need ports 80/443. Administrative panels do not.
  2. Use Tailscale for administration, not as blanket trust. Tailnet access is useful, but individual devices and servers still need firewalls and access rules.
  3. Separate website hosting from app experimentation. Hestia handles stable WordPress sites. Coolify handles custom applications and agent automations.
  4. Keep containers away from host control. Do not mount the Docker socket, run in host network mode, or run privileged containers unless strictly necessary.
  5. Limit container egress. App containers should be prevented from making outbound requests to private management IPs by default.
  6. Treat secrets as the real perimeter. API keys, tokens, session profiles, and databases require stricter access restrictions than codebase files.
  7. Use Git as operational memory. Deployments should track specific Git commits. Code changes belong in repositories, not edited directly on servers.
  8. Prefer simple deployments. If a tool is small and runs client-side, host it directly inside WordPress to avoid runtime overhead.
  9. Use standalone app deployment for separate lifecycles. Complex applications (like AeroLab and Weibull analyzer) warrant their own build pipelines, containers, and repositories.
  10. Make updates visible. Actively track repositories, align package sources, and ensure server upgrades are smooth and patchable.

Conclusion

The result is a setup that lets me publish WordPress sites, educational tools, and containerized apps without exposing every management interface to the internet.

It is not over-engineered. The important parts are explicit boundaries: public web traffic stays public, administration stays inside the tailnet, WordPress publishing and app deployment use different platforms, app containers do not get host-level control, unused services are removed, secrets are treated as sensitive state, source code and deployment revisions are tracked in GitHub, and package maintenance remains clean.

That balance is what makes the infrastructure useful for my work: quick enough for installing new websites and web apps, but structured enough that it remains secure and maintainable over time.