How to deploy Docker without managing a server
Follow this on your own server. Start free, no card needed
docker build works. docker run -p 3000:3000 myapp works on your laptop. What doesn’t exist yet is everything between “this container runs” and “this is a production service”: a reverse proxy in front of it, a certificate that renews itself, a process that restarts after a crash, a server whose operating system gets patched, and someone or something watching whether it’s actually up. None of that is specific to your app. All of it is specific to running a server, which is the part a lot of people with a working Dockerfile would rather not learn from scratch.
This guide is for that situation: you have a Dockerfile, it builds, and you want it running in production without becoming the person who maintains a Linux box. It covers what “managing a server” actually involves, the manual version of doing it yourself (briefly, since that’s not the point), and then the path that removes it.
What “managing a server” actually means
Each of these is a small, well-understood problem on its own. Together, for one container, they’re a disproportionate amount of ongoing work:
- OS updates. Security patches for the base Ubuntu or Debian install itself, independent of anything in your Dockerfile, need applying on some cadence, and some of them need a reboot to take effect.
- Installing and maintaining Docker. The Docker Engine on the host is its own piece of software with its own upgrade path, separate from any image you run on top of it.
- A reverse proxy. Something has to terminate HTTPS and forward requests to your container’s port; that’s nginx or Caddy configuration, written and kept in sync with your app by hand.
- Certificate renewal. Let’s Encrypt certificates expire every 90 days. Something needs to request the first one, prove domain ownership, and renew it before it lapses, indefinitely, without anyone remembering to do it.
- Monitoring. Knowing the container is actually serving traffic, not just that
docker psshows it running, needs something checking from outside the box. - Restart policy and firewall rules. A crash needs to bring the process back without a human SSHing in, and the box needs only the ports it actually serves open to the internet.
None of this is hard in isolation. It’s the kind of work that’s invisible until the one week it isn’t: a certificate silently expires, an unpatched CVE sits open for months, or a crash at 2 a.m. just stays crashed.
The manual version, briefly
On an Ubuntu 24.04 VPS, this is Docker plus Caddy as a reverse proxy:
curl -fsSL https://get.docker.com | sh
A Caddyfile in front of the container:
myapp.example.com {
reverse_proxy localhost:3000
}
docker run -d --name myapp --restart unless-stopped -p 3000:3000 --env-file .env myapp:latest
caddy run --config Caddyfile
Caddy requests and renews its own Let’s Encrypt certificate automatically once DNS points at the server and ports 80/443 are reachable, which covers certificate renewal. --restart unless-stopped covers the crash-recovery piece. What’s left, and what this short version doesn’t solve, is everything in the list above that isn’t the container itself: OS patches on a schedule, Docker’s own upgrades, ufw rules kept in sync with what’s actually running, and something external checking the site is actually reachable, none of which come from running Caddy and Docker correctly.
The path that handles the rest
Point Railyard at the same repository, or at an existing image. Railyard’s build detection picks a Dockerfile automatically when the repo has one that actually builds the app, a development image that only expects a bind mount is skipped rather than deployed; see build methods. For an image already sitting in a registry instead of a repo, image://ghcr.io/you/myapp:latest as the source works the same way, and the port your Dockerfile EXPOSEs is read straight from the registry, no manual port configuration; see deploying a Docker image.
What that one step actually replaces, against the manual list above:
- The reverse proxy and TLS certificate aren’t something you write. Every app gets
https://<name>.<server-ip>.sslip.iowith a certificate immediately; pointing your own domain at it adds a DNS record and an ownership check (a TXT record Railyard verifies before issuing anything, so no one can point a domain they don’t control at your app), and the certificate from there is automatic. See domains and HTTPS. - OS updates show up on the server’s own page, with a count of pending updates and how many are security-specific; nightly security updates apply unattended, and nothing restarts the box without you choosing to. See server maintenance.
- Restart policy isn’t a flag you remember to add. It’s just how every app’s container runs.
- Deploys themselves don’t cause downtime: a new container is pulled or built, the release command (if you set one) runs, and traffic only swaps to it once the new container actually answers, the same zero-downtime shape whether the image changed or the code did.
- A registry-based image that updates on its own tag can redeploy automatically: turn on Deploy when the tag changes under Settings → Source, and Railyard checks the registry every five minutes (or instantly, via a webhook you add to the registry) and redeploys when the digest moves, so
docker push-ing a newlatestis the whole release process. See deploying a Docker image.
One thing worth being precise about: Railyard isn’t a serverless container platform that owns the hardware for you. You still add a server, either one it creates on your DigitalOcean, Hetzner, Vultr, AWS or OVH account, or any Ubuntu 22.04/24.04 box you already have SSH access to, and your cloud provider bills you directly for the box itself. See add a server. What goes away is everything on top of owning that box: installing Docker on it, writing and maintaining the reverse proxy config, requesting and renewing certificates, and patching the OS by hand.
Variables and secrets
Whatever your container reads from its environment, set it under the app’s Variables tab; values are encrypted and applied on the next deploy. If your image needs a credential that genuinely has to be a file rather than a variable (a service-account JSON, a client certificate), Variables → File secrets mounts it read-only inside the container and exposes its path as an environment variable, so GOOGLE_APPLICATION_CREDENTIALS=${RAILYARD_SECRET_FILE_GCP_KEY_JSON} resolves without a manual file copy to the server. See variables and secrets.
Troubleshooting
The deploy succeeds but the app never comes up at the domain. Check the domain’s status on the app’s Domains tab: unverified means the TXT ownership record isn’t found yet (DNS propagation, or a typo in the record), misconfigured means certificate issuing failed and the card explains why, and unrouted means the domain verified but the app hasn’t redeployed since, which a redeploy fixes.
A Dockerfile that works locally fails to build. The most common gap between local and Railyard builds is an image that assumes a bind-mounted source directory rather than COPY-ing the app into the image; that pattern is exactly what Railyard’s build detection treats as a development image and skips. Confirm the Dockerfile builds a self-contained image with docker build . and no -v flags before troubleshooting further.
The build runs out of memory. Large builds are fitted automatically to a temporary, larger build server when the provider allows it, so this is rare, but when it happens the deploy page says so directly and suggests the next step rather than failing silently.
FAQ
Do I still need to know Docker at all? Yes, in the sense that you still write the Dockerfile; what this removes is everything downstream of a working image, not the image itself. If your Dockerfile already builds and runs locally, that part of the work is done.
Is this the same as a serverless container platform like Cloud Run? No. Those platforms run your container on infrastructure they own and bill you per request or per second of compute. Railyard runs it on a server you add to your own cloud account (or a box you already have), billed by your provider, and automates what running a server on top of that box involves.
What if my image needs more than one port, like a metrics endpoint alongside the main app? Settings → General → More public ports takes additional name:port pairs, each getting its own HTTPS address and certificate the same way the main one does; see networking.
Does my container need to run as root? No, and it’s better if it doesn’t; that’s a property of your own Dockerfile, not something Railyard requires either way. A separate, opt-in Hardened isolation setting under Settings → General further restricts what a container can reach on a server it shares with other apps, worth testing against an image that doesn’t assume root.
What actually happens on a deploy, step by step? The image is pulled or built, any release command you’ve set runs against it, and only once the new container answers does traffic swap over to it; the old container stays up until that handoff succeeds, so a bad deploy doesn’t take the previous working version down with it.
The Dockerfile was never the hard part. Everything downstream of it, the box it runs on staying patched, the certificate staying valid, and the process coming back after it crashes, is what “managing a server” means in practice, and it’s also the part that has nothing to do with what your container actually does.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.