You have shipped a critical feature and need to run a database migration before the marketing announcement goes live. In your previous job, you would SSH into the production server, navigate to the application directory, and run the migration command. Simple enough. But now you are deploying with containers, and when you try to SSH into your "server," you realize there is no server to connect to. At least not in the way you remember.
This moment of confusion is almost universal for developers transitioning from traditional server deployments to container-based platforms. The good news is that running maintenance tasks in containers is actually simpler than the SSH workflow you are used to. You just need to shift your mental model slightly. The convox run command gives you everything you need to execute one-off tasks, and it works without requiring any Kubernetes knowledge.
When you deployed to a traditional server, that server had a persistent identity. It ran continuously, maintained state on disk, and you could connect to it anytime via SSH. Your application lived on that machine alongside your data, logs, and configuration files. Running a migration meant connecting to that machine and executing a command in the same environment where your application ran.
Containers flip this model entirely. A container is a process, not a server. It starts, runs your application, and can be replaced at any time by another identical container. There is no persistent filesystem to SSH into. There is no single machine that "is" your application. Instead, your application might be running across multiple containers that can be created, destroyed, and rescheduled across different underlying machines without your involvement.
This ephemerality is actually a feature, not a limitation. It enables zero-downtime deployments, automatic recovery from failures, and horizontal scaling. But it does mean that the "SSH into the server" pattern no longer applies. You cannot SSH into a container because by the time you connect, that container might be gone, replaced by a new one.
The replacement pattern is to spin up a new container specifically for your one-off task. This container uses the same image and environment variables as your running application, executes your command, and then exits. Convox makes this trivially easy with the convox run command.
The convox run command creates a new container from your application's image, injects all your environment variables, and executes whatever command you specify. The basic syntax is straightforward:
$ convox run <service> <command>
The <service> argument specifies which service from your convox.yml to use as the base image. This matters because different services might have different Dockerfiles, dependencies, or configurations. If you have a web service and a worker service, running a migration through the web service ensures you have the same dependencies available that your web application uses.
When you run this command, Convox does several things behind the scenes. It pulls the image from your most recent release, creates a new container, mounts any necessary volumes, injects all environment variables (including database connection strings), and starts your command. When the command finishes, the container is cleaned up automatically.
For Convox Cloud deployments, the command syntax adds the machine identifier:
$ convox cloud run <service> <command> -a <app> -i <machine>
Both variants work identically in terms of what they accomplish. The difference is just which infrastructure you are targeting. See the run CLI reference for the complete list of options.
Let us walk through the most common maintenance task execution scenarios you will encounter. These are copy-paste commands that you can adapt to your specific stack.
Database migrations are probably the most frequent one-off task you will run. The exact command depends on your framework, but the pattern is always the same.
Ruby on Rails:
$ convox run web "rake db:migrate" -a myapp
Django:
$ convox run web "python manage.py migrate" -a myapp
Node.js with Prisma:
$ convox run web "npx prisma migrate deploy" -a myapp
Node.js with Knex:
$ convox run web "npx knex migrate:latest" -a myapp
The migration command runs against your production database using the DATABASE_URL environment variable that Convox automatically injects. No need to manually specify connection strings or worry about credentials.
Sometimes you need an interactive session to debug an issue or explore data. The convox run command handles this seamlessly.
Rails console:
$ convox run web "rails console" -a myapp
Django shell:
$ convox run web "python manage.py shell" -a myapp
Node.js REPL with your app loaded:
$ convox run web "node" -a myapp
General bash shell for exploration:
$ convox run web bash -a myapp
Interactive sessions give you the same experience you would have had when SSHing into a server, except now you are in a fresh container with the exact same configuration as your production application.
When you need to run a data backfill or cleanup script, use the service that has access to the relevant code and dependencies.
Node.js backfill script:
$ convox run worker "node scripts/backfill-user-profiles.js" -a myapp
Python data cleanup:
$ convox run worker "python scripts/cleanup_stale_sessions.py" -a myapp
Ruby rake task:
$ convox run worker "rake users:sync_stripe_subscriptions" -a myapp
Note that you can run these against any service in your convox.yml. If your worker service has different dependencies than your web service, choose accordingly.
By default, convox run runs interactively. Your terminal attaches to the container, you see the output in real time, and the command blocks until it completes. This is perfect for quick migrations, console sessions, and short scripts.
But what about a script that takes 30 minutes to run? You do not want to keep your terminal open that long, and if your connection drops, the process might be interrupted. For these situations, use the --detach flag:
$ convox run worker "node scripts/reindex-search.js" --detach -a myapp
Running detached process... OK, web-abcd1234
The command returns immediately with a process ID. The script continues running in the background on the cluster. You can close your laptop, go get coffee, and the job keeps going.
To check on your detached processes, use convox ps:
$ convox ps -a myapp
ID SERVICE STATUS RELEASE STARTED
web-abc123 web running RABCDEFGHI 2 hours ago
web-def456 web running RABCDEFGHI 2 hours ago
web-abcd1234 web running RABCDEFGHI 5 minutes ago node scripts/reindex-search.js
You can also stream the logs from your detached process:
$ convox logs -a myapp --filter "web-abcd1234"
Here is a simple decision tree for choosing between interactive and detached mode:
Some one-off tasks need more resources than your typical web request. A data migration that processes millions of records might need extra memory. A report generation script might be CPU intensive. You can allocate additional resources with the --cpu and --memory flags:
$ convox run worker "node scripts/heavy-backfill.js" --cpu 1000 --memory 2048 --detach -a myapp
The --cpu flag specifies CPU in millicores (1000 equals one full CPU core), and --memory specifies memory in megabytes. This ensures your resource-intensive task gets the capacity it needs without affecting your running application.
When you are the only person handling ops, you might keep these commands in your head or scattered across Slack messages. But even solo operators benefit from documenting their operational procedures. A simple runbook in your project repository makes it easy to remember the exact commands and ensures consistency.
Create a RUNBOOK.md file in your repository root:
# Operations Runbook
## Database Migrations
# Staging
convox run web "npx prisma migrate deploy" -a myapp-staging
# Production (always run on staging first)
convox run web "npx prisma migrate deploy" -a myapp-production
## Console Access
# Production Rails console (read-only recommended)
convox run web "rails console --sandbox" -a myapp-production
## Common Data Tasks
# Reindex search (runs in background, takes ~20 minutes)
convox run worker "node scripts/reindex-search.js" --detach -a myapp-production
# Sync Stripe subscriptions
convox run worker "rake stripe:sync_subscriptions" -a myapp-production
## Checking Background Tasks
# List running processes
convox ps -a myapp-production
# View logs for a specific process
convox logs -a myapp-production --filter "process-id"
This runbook becomes invaluable when you are debugging at 2 AM and cannot remember the exact command syntax. It also serves as documentation if you ever bring on additional team members or hand off ops responsibilities.
The transition from SSH-based operations to container exec patterns is one of those changes that feels unfamiliar at first but quickly becomes natural. Instead of connecting to a persistent server and navigating filesystems, you simply tell Convox what command to run and it handles the rest.
The convox run command gives you the same operational capabilities you had with SSH, without requiring you to understand Kubernetes pod scheduling, container networking, or cluster architecture. You run a command. Convox figures out where and how to execute it. The output appears in your terminal.
For teams without dedicated DevOps staff, this simplicity is the whole point. You should be spending your time building features, not learning container orchestration primitives. Codify your common operations into a runbook, and you will have a reliable playbook for maintenance task execution that anyone on your team can follow.
Questions about migrating your operational workflows? Contact us.