All posts

8 min read Nathan Broadbent

Eight years of running DocSpring on Convox

Convox is a Heroku-like PaaS in your own AWS account. We've been running DocSpring on it since 2018.

A terminal showing convox deploy connects to three running application windows on a shared platform.

I migrated DocSpring from Heroku to AWS in 2018. (It was still called FormAPI back then.) We were given some free AWS credits, and I wanted to use them without having to build a deployment platform myself. Convox gave me a working platform out of the box: describe the application, deploy it, and get back to building the product.

Convox is convention over configuration for infrastructure. I started with a platform I could use before I understood all of its implementation. I learned from that implementation, took over more of the Terraform configuration, and adapted it to our requirements while keeping the application workflow.

Here's a Hacker News comment I wrote in July 2018 about the move. I was enthusiastic about rolling deployments and being able to use our AWS credits. Since then, our infrastructure has moved from ECS to Kubernetes, we've built out our own security and compliance controls, and we've added our own access layer.

Convox was founded by David Dollar, Noah Zoschke and Matt Manning, who all came from Heroku. So Convox might feel very familiar if you've already used Heroku. Convox calls an installation of the platform a rack. The open-source rack is Apache-2.0 licensed, runs in your cloud account, and has a documented CLI installation path. The separate Convox Console adds a web interface and team workflows, with free and paid plans. You can use the rack independently.

Today, our team runs a production rack in the US, another in the EU, and a staging rack. We also help maintain Convox racks for some of our Enterprise customers.

What an ordinary deployment looks like

For a Rails application, you will usually need a web process and a background worker, and some scheduled commands. Convox lets you describe those in convox.yml.

This simplified configuration assumes an existing Rails app with a Dockerfile, Sidekiq, a health endpoint at /up, and a reports:daily rake task. Its database and Redis are provisioned separately:

environment:
  - RAILS_ENV=production
  - SECRET_KEY_BASE
  - DATABASE_URL
  - REDIS_URL

services:
  web:
    build: .
    command: bundle exec rails server -b 0.0.0.0 -p 3000
    port: 3000
    health: /up
    scale:
      count: 2
  worker:
    build: .
    command: bundle exec sidekiq

timers:
  daily-report:
    schedule: "0 3 * * *"
    service: worker
    command: bundle exec rake reports:daily

This config declares two web processes and a worker. The timer starts a separate process using the worker service's configuration at 03:00 UTC. Environment entries without defaults must be supplied through the app's configuration; their values stay out of this file.

The CLI remembers your selected rack and picks the app from your current directory:

convox deploy
convox logs
convox run web bundle exec rails console

The first command builds the containers, creates a release pairing the build with the app's environment variables, and promotes it through a rolling deployment. New processes start and pass health checks as the old ones are replaced. Startup or health-check failures can cause the rollout to reverse to the previous release. The other commands let you follow the application's logs and start a Rails console.

The convox.yml manifest also handles custom domains, resource limits and autoscaling, and Convox manages Let's Encrypt certificates. Its resources can provision databases and caches: postgres runs in a container, while rds-postgres creates an AWS-managed database. You can also connect to databases you manage separately.

Growing into Terraform

I didn't have a lot of experience with AWS or DevOps when I first started. I've learned a great deal by studying Convox and figuring out how to modify and extend it.

Convox v2 used CloudFormation and Amazon ECS. In February 2020, Convox announced its Kubernetes-based multi-cloud platform, installed through Terraform. On AWS, v3 uses EKS, Amazon's managed Kubernetes service. It also supports Google Cloud, Azure and DigitalOcean.

I started managing our rack Terraform directly when I wanted to add CloudWatch configuration and customize certificate DNS validation. I also wanted our database and storage infrastructure managed in the same place. Starting with Convox meant I could make those changes around an application platform that was already working.

Convox's AWS system module brings together the cluster, rack API and logging. Its cluster module manages networking and EKS. Nodes run in private subnets behind NAT gateways by default; management endpoints have separate access settings.

You can use those modules from your own infrastructure repository. This excerpt pins the module and rack release together and chooses the Contour ingress controller for a new rack:

module "rack" {
  source = "git::https://github.com/convox/convox.git//terraform/system/aws?ref=3.26.0"

  name        = "example-staging"
  region      = "us-east-1"
  release     = "3.26.0"
  private     = true
  router_type = "contour"
}

The examples use Convox 3.26.0. Our deployment currently uses a customized 3.25.14 base. A complete Terraform setup will also need credentials, state storage and access configuration.

I eventually wrote quite a lot of my own custom Terraform. By then I understood much more of what I was changing, and I still had Convox's builds, releases and application API underneath our workflow. (I also use Terraform to manage everything in Cloudflare, Tailscale, CircleCI, all of our monitoring tools, and many other things.)

Our access controls

Our SOC 2 work brought requirements for individual permissions, approval of sensitive operations, and evidence of what happened. We chose to build Rack Gateway, an independent proxy for the Convox API, and make it accessible through our private Tailscale network. Those are additions for our own workflow; a new Convox rack doesn't require them. The official Convox Console offers its own team-access and audit features, depending on the plan.

Rack Gateway now handles security-key approval for agent commands and deployments. I can review the Ruby an agent wants to run, and our command configuration uses a read-only database user by default. Deploy approvals release the matching CircleCI hold. Our Rack Gateway post goes into more detail about the implementation and audit trail.

Convox's API let us add those controls while keeping its deployment system. They support our SOC 2 program alongside our policies and operational procedures.

Convox architecture: the standard CLI and Console connect to the rack API; DocSpring adds private Tailscale access and Rack Gateway approvals. Application users connect through the ingress.

The top path shows the standard management interface. In DocSpring's setup, management requests go through our gateway, and we restrict direct access to the rack API. Application traffic goes through the ingress in either case.

What we still maintain

Managing your own infrastructure comes with a lot of responsibility. We own the upgrade schedule, monitoring, backups and restore testing, along with our Terraform customizations. Larger projects, such as replacing an ingress controller, are separate from the day-to-day work of deploying and operating the application.

Lately, tightening our security and compliance position has involved substantial work: Redis TLS, protecting backups and audit logs from deletion, retaining useful incident logs, and changing network access controls.

Replacing ingress-nginx was another substantial job. The upstream project retired in March 2026. Convox ships its own build and already supports Contour/Envoy, but its documented switching procedure takes apps offline until they're redeployed. We built a zero-downtime transition in our fork and used it to move our racks to Contour.

The transition had to account for connections that Cloudflare kept open to nginx, even after new connections were going to Envoy. We waited for nginx's access log to go quiet before removing the old routes. We also added Envoy access-log collection and redaction, and fixed two certificate-handling bugs. Those changes are in our fork, and we've offered them upstream.

Convox is actively releasing updates: 3.26.0 came out on November 5. When I submitted a fix for how rolling deployments spread pods across zones, a maintainer replied that they were including it in 3.25.14. I can contribute a fix when I need one, but I also have to maintain it while it's specific to our setup.

What a three-AZ AWS rack costs

Running a Convox rack in your own AWS account comes with an AWS bill, and it can be a lot higher than running your application on a VPS, especially when using a cheap provider like Hetzner. Here is an illustrative estimate for one configuration, using US East (N. Virginia) pricing.

Multiple small applications can share a rack while capacity allows, spreading its fixed costs across those apps. This configuration uses three availability zones, private nodes and a public router:

Component Example calculation Monthly cost (USD)
EKS control plane, standard version support 1 × $0.10/hour $73.00
NAT gateways 3 × $0.045/hour $98.55
Network Load Balancer, fixed charge 1 × $0.0225/hour $16.43
Public IPv4 addresses for NAT and the router 6 × $0.005/hour $21.90
Linux t3.medium worker nodes 3 × $0.0418/hour $91.54
Node boot disks, gp3 at baseline performance 3 × 20 GiB × $0.08/GiB-month $4.80
Subtotal before usage charges and databases About $306/month

Sources: AWS's EKS, NAT and IPv4, load balancer, T3 and gp3 pricing.

About $210 of that is the control plane and fixed networking charges. The workers host both Convox's services and your application; the remaining capacity is what you can use for workloads. Add databases, backups, image storage, logging, traffic charges, load-balancer capacity charges and any burst CPU charges separately. Autoscaling can also add nodes. EKS extended-version support costs more than standard support, which is a good reason to keep Convox up to date and use the latest supported Kubernetes version.

A separate staging rack has its own baseline costs. A hosted platform is often a better starting point when you don't need to control the cloud account.

Would I choose it today?

I'd choose Convox again for a team that wants Heroku-style deployments in its own cloud account, with infrastructure it can customize as requirements grow. The open-source rack and its Terraform modules give you a foundation you can inspect and extend. Convox is what we recommend for our Enterprise on-premises installations, although our customers are free to run DocSpring's Docker image however they like.

Our security and compliance work is one example of that growth. For teams with strict SOC 2 or HIPAA requirements, Aptible is another solid option I'd consider. Familiarity is part of my preference for Convox: I know how it works and where to make changes, and our operational experience is on AWS.

Try it as an experiment

I'd start with Convox's Apache httpd example, so your first experiment doesn't need a database or real application data. Use a sandbox AWS account, the AWS CLI, Terraform and the Convox CLI. Follow the credentials and permissions setup in the CLI installation guide, then install a dedicated rack:

convox rack install aws convox-evaluation --version 3.26.0 \
  region=us-east-1 node_type=t3.medium private=true \
  high_availability=true router_type=contour

convox switch convox-evaluation
git clone https://github.com/convox-examples/httpd.git example-httpd
cd example-httpd
git checkout c37afd4a6f7c799f044b71ff895ef81d592dd46b
convox apps create
convox deploy
convox services
convox logs

It takes about 5-20 minutes to provision a rack, depending on the provider; allow additional time for setup, deploying and cleanup. Using the configuration above, four hours is roughly $2 in fixed charges before usage. I'd budget $10 for a short evaluation and check the bill afterwards.

When finished, remove the sample app and the dedicated rack:

convox apps delete example-httpd
convox rack uninstall convox-evaluation

Wait for deletion to finish and check for retained AWS resources, including storage and logs.

That experiment should tell you whether you like the daily workflow. Eight years ago, that was enough to get me started. Having the implementation available to study and extend is why we're still using it.

More posts

  1. Twelve terminal cards on the left all connect to one brass key in a navy box labelled localhost, which connects to five round service icons on the right.

    10 min read

    Why Claude Code MCP servers keep asking you to sign in again

    Run dozens of Claude Code sessions and they sign each other out of your MCP servers, because each OAuth refresh token works only once and the sessions race to use it. One local MCPProxy now holds the tokens for all of them. Why it happens, what we changed in our fork, and the macOS setup.

  2. Four git worktree cards each hold dashed outline copies of the same crates and one crate of their own, linked to a single stack of crates labelled cache.

    6 min read

    Sharing Rust builds across git worktrees with kache

    My shared Cargo build directory needed an unstable Cargo feature and made every worktree wait on one lock. kache shares the same builds on stable Cargo, with less disk and faster builds, once one coverage flag was out of the way.

Try DocSpring with your own documents

Upload a PDF or a Word document, or create an HTML template, then send a test request. No credit card needed.

Blog posts and product news by email

New articles and product changes, once a month.