You already have the hard part: a product that works on your laptop. docker compose up brings up the app, Postgres, maybe Redis and a MinIO container pretending to be S3. What you don't have is a place to run it where nothing wakes you up at 2 AM and nothing produces a bill you can't explain.
That's what managed cloud services are for. This post is a timeboxed plan: two hours, four blocks, from docker-compose.yml to a production URL your first ten beta users can hit. It's opinionated, because at your stage having an opinion saves more time than keeping options open.
What "managed" should mean for a solo founder
In enterprise IT, managed cloud services usually means handing a third party the day-to-day running of your cloud: patching, monitoring, backups, security, sometimes a whole team on retainer. You don't need a retainer. You need the same outcome in product form:
- Someone else patches the OS and the database engine.
- Someone else takes backups and keeps them somewhere other than the disk that just died.
- Someone else handles TLS, load balancing and restarts when a container crashes.
- You deploy by pushing code, not by SSHing into anything.
If a provider leaves any of these to you, it's hosting with a nicer UI, not a managed service. That's fine for some people. It isn't fine if you're the only engineer and launch is in six weeks.
Rule of thumb: count the things that can page you. A $20 droplet running app plus Postgres plus Nginx has at least six (disk, memory, OS updates, certs, backups, the process itself). A managed setup should get that down to one: your own code.
Before the clock starts: read your compose file differently
Open docker-compose.yml. Every service in it falls into one of two buckets.
services:
web:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://cache:6379
S3_ENDPOINT: http://minio:9000
depends_on: [db, cache, minio]
worker:
build: .
command: node dist/worker.js
db:
image: postgres:16
volumes: [pgdata:/var/lib/postgresql/data]
cache:
image: redis:7
minio:
image: minio/minio
command: server /data
volumes:
pgdata:- Your code (
web,worker): these become app services, deployed from your repo or image. - Stateful infrastructure (
db,minio, oftencache): these should not be containers you run in production. They become managed services.
That split is the key idea of the whole migration. The most common mistake is deploying the entire compose file as-is to a VPS, with Postgres in a container on the same disk as everything else. That's exactly how you end up with a 6-hour outage when the disk fills up.
Block 1 (0:00–0:30): make the app cloud-shaped
Thirty minutes of changes in the codebase save hours later.
1. Everything configurable from the environment. Search for hard-coded hostnames:
grep -rnE "localhost|127\.0\.0\.1|minio:9000|db:5432" src/Each hit should read from process.env (or your language's equivalent), with local defaults living only in .env files that aren't committed.
2. Listen on $PORT.
const port = Number(process.env.PORT) || 3000;
app.listen(port, "0.0.0.0");Binding to 0.0.0.0 rather than 127.0.0.1 matters inside a container.
3. A production Dockerfile. If build: . already uses a multi-stage Dockerfile with a production start command, you're done. If it runs npm run dev or mounts your source as a volume, fix that now. Production images shouldn't depend on bind mounts.
4. Uploads go to S3, not disk. If you already talk to MinIO through the AWS SDK, you're 95% there. Make sure the endpoint, bucket and credentials come from environment variables, and set forcePathStyle according to what the provider expects.
import { S3Client } from "@aws-sdk/client-s3";
export const s3 = new S3Client({
region: process.env.S3_REGION ?? "us-east-1",
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});5. A health endpoint. GET /healthz that returns 200 when the app can reach its database.
Commit and push. Block 1 done.
Block 2 (0:30–1:00): provision the managed pieces
Now create the stateful services, before deploying the app, so their connection details exist when you need them.
Managed database
Create a managed Postgres (or MySQL) with the same major version you run locally. Then check three things, whichever provider you use:
- Automatic backups are on, and you know the retention period.
- You can restore. Find the restore button or doc now, not during an incident.
- Connection limits. Small plans cap connections. Set your ORM pool to something modest, like 5–10 per app instance.
If you have local data worth keeping (seed data, a demo account for investors), move it with pg_dump/pg_restore:
docker compose exec -T db pg_dump -U app -Fc app > local.dump
docker run --rm -v "$PWD":/w postgres:16 \
pg_restore --no-owner --no-acl -d "$MANAGED_DATABASE_URL" /w/local.dumpObject storage and CDN
Create a bucket in S3-compatible storage. Generate an access key scoped to that bucket. If the provider puts a CDN in front of the bucket, note the public CDN URL. That's what you'll use for user-facing asset links instead of raw bucket URLs.
Redis: do you really need it?
Be honest here. If Redis is only a cache, you can often drop it for launch and add it later. If it's backing a job queue (BullMQ, Sidekiq, Celery), you need it. Use a managed Redis if your provider has one, or a dedicated managed Redis service. Don't run it in a plain container without persistence for a queue you care about.
Block 3 (1:00–1:30): deploy the app
With a platform that deploys from Git or a Docker image, this block is mostly waiting for the first build.
Here's how it goes on Granite.so, which covers every piece above on one dashboard:
1. Connect your GitHub or GitLab repo and pick the production branch. Builds run automatically on every push from your existing Dockerfile.
2. Set environment variables: the managed database URL, S3 endpoint, keys and bucket, the CDN base URL, and your app secrets.
3. Add the worker as a second service from the same repo with a different start command (the command: from your compose file).
4. Deploy and watch the build logs.
When it's green, open the URL and click through the core flow. The usual first-deploy failures and their fixes:
| Symptom | Likely cause |
|---|---|
| Container starts, then is killed | App not listening on $PORT or on 0.0.0.0 |
ECONNREFUSED to the database | A leftover db:5432 hostname in config |
too many connections | ORM pool too large for the plan |
| Uploads return 403 | Wrong bucket policy, or forcePathStyle mismatch |
| Works, but slow first request | Migrations running on first boot, which is fine |
Deploying on a managed platform, you won't write any Nginx, certbot, systemd or firewall config. That's the part of the two hours you don't spend.
Block 4 (1:30–2:00): make it production, not just "up"
Being up isn't enough. Spend the last half hour on the things that make it survive a week without you.
1. Custom domain. Add it in the dashboard and create the DNS record your provider asks for. TLS should be issued automatically.
2. Test the backup. Restore last night's (or the first) automatic backup into a scratch database and run a SELECT count(*) on a key table. A backup you haven't restored is a hope.
3. Uptime check. Point a free external monitor at /healthz. You want to find out before your users do.
4. Error tracking. Add Sentry or similar. That's ten minutes and a DSN in an environment variable.
5. Give your co-founder access. A non-engineer co-founder should be able to see that the app is up and redeploy the last good version without calling you. Show them where that button is.
6. Write down the monthly number. Literally, on a sticky note.
That last step is the one most founders skip, and it's the one that decides whether this setup lowers your anxiety or not.
The cost side: predictable beats cheap
The cheapest option on paper is still a $5–20 VPS. It stays cheapest right up until you count the hours: a few per week on maintenance, plus the one night it breaks. At pre-seed your time is the most expensive resource you have.
The other trap is pure metering. Usage-based pricing is fair in principle, but a database that quietly grows its memory footprint turns into a bill you have to investigate. Ideally you want a known base price with a clear allowance.
For reference, Granite.so's Hobby plan is $9/month and includes $15 of usage credits, which is typically enough for a small app, a worker and a managed database at beta-user traffic. There are larger plans if you outgrow it. Check the current pricing page, and keep an eye on the usage view for the first month so you learn your real baseline.
Honest caveat: "Managed" doesn't mean "no responsibility". You still own your schema, your queries, your secrets and your data retention choices. A slow query is still your problem. It just won't take the server down with it.
When managed cloud services are the wrong call
In fairness, there are cases where this isn't the right move:
- You need root on the machine, for custom kernel modules, unusual networking, or GPU drivers the platform doesn't support.
- Your workload is huge and steady. At large, predictable scale, reserved raw compute plus a platform team can be cheaper.
- Compliance requires a specific region or certification that the provider doesn't offer. Check before you commit.
For a pre-seed MVP with ten beta users, none of these usually apply.
Your two-hour checklist
- 0:00 Hard-coded hosts removed, app reads
$PORT, productionDockerfile - 0:20 Uploads go through the S3 SDK with env-based config,
/healthzadded - 0:30 Managed database created, backups confirmed, data restored
- 0:45 Bucket, scoped keys and CDN URL ready; Redis decision made
- 1:00 Repo connected, variables set, web and worker deploying
- 1:30 Custom domain and TLS live
- 1:40 Backup restore tested, uptime monitor and error tracking on
- 1:55 Co-founder has access, monthly cost written down
Stop managing servers, start shipping
The real deliverable of these two hours isn't a URL. It's getting a whole category of problems off your list: disks, certificates, backups, patches, and the late-night dread that comes with them.
If you want to do the whole thing on one dashboard, with app hosting from your Dockerfile, a managed database with automatic backups, and S3-compatible storage with a CDN, start on Granite.so with the Hobby plan and run through the checklist above. You'll be back to building product before lunch.