Deploy Django with Kamal on your own servers
31/03/2026
Your Django application works locally, passes tests, and the team wants to deploy it to production. But between "it works on my machine" and "it runs stably on a server" lies a territory full of decisions that are rarely well explained. Kamal fits right into that in-between space: when manual scripts no longer scale, but Kubernetes is still more than you need.
You have a Django project that's well past the prototype phase, with real users or soon to have them, and each new version still depends on a sequence of steps that only one person on the team understands from beginning to end. Someone logs into the server via SSH, pulls the code, restarts the service, checks that the website loads, and hopes that nothing has broken along the way.
That model can work for a while. The problem isn't that manual deployment is impossible, but that it's difficult to reliably replicate. When a step changes and isn't documented, the process becomes more fragile. As the team grows, that accumulated fragility starts to become a real risk: failed deployments, unnecessary downtime, and a general feeling that everything is held together by a thread.
Kamal emerges as a response to that specific scenario. Not as an orchestration platform, nor as a replacement for your existing infrastructure, but as a way to standardize how your application is built, published, and updated on servers you directly control.
What is Kamal and what problem does it solve when deploying Django?
Kamal is a deployment tool for containerized applications. It doesn't create infrastructure or design your architecture; it introduces a repeatable process for publishing new versions on machines you already own and manage. Developed by the 37signals team, it arose from a very specific need: deploying web applications on your own servers without relying on managed platforms or setting up a Kubernetes cluster. Between these two extremes, there was a gap where most small and medium-sized teams relied on handcrafted scripts that worked until they stopped.
Kamal's real value lies in the operational discipline it introduces to the deployment cycle: building the image, publishing it to a registry, deploying it to the target servers, and coordinating related services in a predictable flow that repeats itself in the same way with each new version without needing to remember intermediate manual steps.
For Django projects deployed on your own servers or VPS, this fit is especially natural because most Django applications share a recognizable architecture: a web service that handles HTTP requests, one or more workers that process asynchronous tasks in the background, a PostgreSQL database, a caching layer with Redis, and an inbound proxy that manages public traffic. Kamal can coordinate all these pieces without forcing you to adopt a platform much larger than you actually need, making it a very reasonable option for that middle ground where manual scripts no longer inspire enough confidence, but Kubernetes would represent a disproportionate leap in complexity.
What Kamal is not. It's important to clarify this from the outset to avoid misleading expectations: Kamal does not replace Docker, reverse proxies, PostgreSQL, or your infrastructure provider. It doesn't design your application's architecture, transform a poorly prepared application into a robust one, or eliminate the need to make decisions about backups, security, observability, or data persistence. Presenting it as "your own PaaS" would be misleading because it's actually a container deployment and coordination tool, not a complete platform that covers the entire lifecycle of a production application.
When does it make sense to use Kamal to deploy Django
Not all projects need Kamal, and recognizing whether yours is a good fit before investing time in configuring it can save you a lot of trouble in the long run. The underlying principle is relatively simple: Kamal is useful when you want to standardize your deployments without taking on the complexity of a larger orchestration platform, and it's less of a good fit when your system has grown to the point where it needs capabilities beyond simply coordinating containers on known servers.
Where it fits
The most common profile is that of small or medium-sized teams that maintain control over their servers—due to cost, operational simplicity, or a preference for a straightforward infrastructure—and work on projects with a moderate architecture: a Django application as the main service, one or more workers, a database, and caching. It's a particularly good fit for products with a regular pace of change, where orderly and reliable deployment is crucial, without the need to change providers, migrate to a managed platform, or set up additional infrastructure that then requires maintenance.
Where it fits worst
Its limitations become apparent in systems with complex architectures: many independent services, uneven scaling, advanced networking needs, multi-region deployments, or strict service isolation requirements. It's also a poor fit for organizations that already rely on a managed platform and don't want to take on the direct operation of servers.
And let's be clear: if no one on your team can or wants to handle server operation—security, backups, failover, system updates, and general maintenance—Kamal doesn't solve that problem. It simplifies deployment, but it doesn't outsource production responsibility. In those cases, a managed platform might make more sense.
"Simple architecture" does not mean a small or unserious project, but rather that the "container plus server plus coordinated deployment" model is still sufficient to represent your real system.
The necessary parts before touching the configuration
Before opening any Kamal configuration file, it's essential to understand the components of your system, the responsibilities of each component, and the relationships between them. Skipping this step is a common cause of deployments that initially appear to work but break after the first reboot or redeploy.
The Django application is the center of the system because it serves web requests, concentrates the business logic, and depends on configuration, secrets, and external services to function correctly, but it is not the only piece that needs attention during deployment, nor the most difficult to manage.
The inbound proxy receives traffic arriving at the public domain, terminates the TLS connection if necessary, and forwards requests to the application's web service. Kamal can manage this layer, but it requires that the domain, DNS, and network configuration are already correctly set up before deployment, because the public access layer doesn't magically appear simply by using a deployment tool.
Workers process asynchronous tasks when the project uses work queues, whether with Celery, Django-RQ, or a similar solution. It's important to understand that workers are completely separate processes from the web service, even though they often share the same container image and codebase: they have their own resource consumption profile, execution pace, and configuration requirements. If your application processes payments in the background, sends emails, generates reports, or executes any type of deferred task, workers are not an optional add-on but an essential part of the deployment. Deploying only the web component, assuming the asynchronous functionality will "work on its own," is a far more frequent source of problems than it might seem.
The database has its own lifecycle, backup strategy, and persistence and recovery needs that shouldn't be treated as just another detail within the application container. The same applies to caching : Redis frequently appears in Django projects to manage queues, sessions, distributed locking, or simply to improve the performance of repeated queries, and in all cases, it requires a deliberate and conscious decision about where it runs, how data is persisted if necessary, and what happens when the service restarts.
Persistent files —media uploads uploaded by users, application-generated assets, or any content that needs to outlast the container's lifecycle—deserve special attention because if the container is replaced with each deployment (which is exactly what happens when you use Kamal), any files stored within it disappear with the previous version, with no possibility of recovery.
The relationship between all these components is also important for deployment. The web service handles traffic and queries the database and cache according to the application's logic; the workers consume tasks from the queue and generally depend on the same base configuration as the web component; the database and cache are infrastructure dependencies with their own lifecycles, not ephemeral components that appear and disappear with each release; and the proxy exposes a stable public interface even though the internal version of the application changes with each deployment.
The key distinction is this: Kamal deploys and coordinates application containers, but it doesn't handle database design, backups, persistence, media and file strategy, or network security on its own. Confusing a deployment tool with a production architecture is one of the most common mistakes.
Prepare the application before the first deployment
Proper deployment begins long before the first command, and most tutorials and quick guides make the mistake of jumping directly to Kamal configuration, implicitly omitting all the prior preparation work, which is precisely where the most difficult errors to diagnose in production are concentrated because they do not appear obviously or generate clear error messages.
Your Django application needs to be production-ready. Simply having the Dockerfile start isn't enough. You need a separate configuration from your local environment, correct ALLOWED_HOSTS, debugging disabled, secure HTTPS and cookies, a clear strategy for collectstatic, useful logging, and health checks to confirm the application is actually working. If your project uses Wagtail, media file management becomes even more critical.
The Docker image must be able to be built consistently and reproducibly. This means an understandable Dockerfile where every instruction has a purpose, clearly defined system dependencies, a build process that doesn't rely on hidden manual steps or local developer configurations, and well-differentiated startup commands for both the web component (Gunicorn or another WSGI server) and the workers. Without a reproducible image that you can build identically at any time and on any machine, Kamal would only automate fragility instead of eliminating it.
Environment variables and secrets must be defined before the first deployment. The SECRET_KEY, database credentials, and external service keys should not be embedded in the image or exposed in the repository. In Kamal, these are typically managed through .kamal/secrets or integrations with external managers, and these values are loaded during deployment or at runtime. Even so, it remains the team's responsibility to decide who has access to them, how they are rotated, and what happens when they change.
Before writing the Kamal configuration, it's advisable to finalize some key decisions. These include: where PostgreSQL and Redis will reside, how file persistence will be handled, how migrations will be executed, and whether the web and worker environments will share the same image. These decisions are easy to postpone because they don't prevent the initial deployment from starting, but they often end up causing problems in production.
External dependencies must also be in place. This includes an accessible container registry for publishing images (Docker Hub, GitHub Container Registry, or a private registry like AWS ECR), SSH access to the target server with correctly configured credentials, resolved TLS certificates and domain pointing to the correct location, and validated connectivity between the server and all the auxiliary services the application requires. If the application relies on external providers for email, file storage, third-party APIs, or external queuing systems, their configuration and accessibility are also crucial for successful deployment.
What's often overlooked at this stage is properly separating static files from media files, giving workers and scheduled tasks their own configuration, and reviewing permissions, timeouts, and the container lifecycle. Without thorough preparation, Kamal only automates a fragile system, so it's essential to focus on having everything you need in place before starting the Kamal configuration.
Translating Django architecture to Kamal configuration
Once the system components are clear and the preliminary decisions have been made, the next step is to model that architecture in the Kamal configuration, and this translation turns out to be one of the most revealing exercises of the whole process because it forces you to make explicit what in other deployment approaches is usually left implicit, scattered among scripts or simply assumed by the person who "knows how everything works".
Roles and servers. In Kamal, it's common practice to separate at least one web role and one worker role. The web role runs Django with Gunicorn or another WSGI server to respond to HTTP requests. The worker role runs Celery, Django-RQ, or the queue used by the project for asynchronous tasks. A single container image can serve both roles by simply changing the startup command, which simplifies releases because there's only one image to build and distribute. However, this requires that these commands be well-defined, documented, and tested independently.
When does everything fit on one host, and when does it stop being reasonable? For a first deployment, or for a Django application with moderate traffic, it might be reasonable to start with the web and worker servers on a single server. When traffic grows, or Celery starts competing with Gunicorn for CPU and memory, separating roles onto different hosts ceases to be an optional improvement and becomes a necessity. The time to think about this separation is now—during the initial configuration—even if you implement it later, because having the roles well-defined from the beginning makes subsequent migration a configuration change rather than a complete restructuring of the deployment.
Image and build. The image must contain everything necessary to run the application reproducibly: the code, dependencies (Python and system), and well-defined startup commands. It is also important that neither secrets nor sensitive configuration are embedded in the image, but rather injected independently at runtime.
Variables, secrets, and configuration. In Kamal, standard configuration and secrets are managed separately. Non-sensitive environment variables can be declared directly in the deployment configuration, while secrets are stored encrypted and injected into containers at runtime by Kamal. This allows you to clearly define which values the Django application needs to start, what the web component and workers share, and what sensitive data should be kept out of the image. In practice, Kamal acts as a bridge between the deployment configuration and the credentials the application needs in production.
Proxy, domain, and TLS. Kamal manages public traffic ingress with kamal-proxy , which is responsible for publishing the application and routing requests to the correct container. However, for it to work properly, you need to have the domain, DNS, and server network configured correctly. If you're using automatic HTTPS, Kamal can obtain certificates with Let's Encrypt when deployed on a single server and the domain correctly points to that machine. It's also advisable to review options such as app_port, host, ssl, forward_headers, and healthcheck, as these are part of how Kamal exposes and validates the application in production.
Volumes and persistence. If your Django application stores media files, uploads, or other persistent data, it can't live inside the container because it's replaced with each deployment. Kamal allows you to declare this data in the configuration using volumes, mounting host paths inside the container so they survive between releases. The alternative is to use external storage. In both cases, the idea is the same: to remove persistent data from the ephemeral lifecycle of the image.
Auxiliary services. Not all system components need to reside within the same Kamal deployment. Deciding what stays in and what stays out is an important part of operational design. PostgreSQL and Redis can be managed as Kamal add-ons or kept separate, but this decision should be made explicitly. In many projects, a reasonable option is to leave PostgreSQL as an external service—because it has its own lifecycle, backups, and maintenance—and use Redis as an add-on tied to the application deployment, since its lifecycle is typically closer to the code and the workers.
Particularities of Django within the deployment model
Django is not just a web container that starts up and responds to requests. To reliably deploy Django with Kamal, you need to consider some components that are part of the release cycle itself.
`collectstatic` must be part of the deployment. Django needs to collect static files before serving the application, so `collectstatic` must have a clear place in the release sequence, typically as a hook. Furthermore, it's advisable to decide in advance how these assets will be served—whether with WhiteNoise, a dedicated proxy, or a CDN—because this decision affects the Dockerfile, the proxy, and subsequent validation. If `collectstatic` fails, the application might start but still render incorrectly.
Migrations require a specific strategy. Simple migrations usually fit well within the normal deployment flow. The problem arises with large or destructive changes: high-volume tables, missing columns, or changes incompatible with the previous code. In these cases, it's advisable to deploy in phases and maintain temporary compatibility between code and schema, because rolling back the code doesn't automatically undo the database migration. If the migration has already been applied and you need to revert to the previous version because something went wrong, the database won't automatically revert to its previous state, and forcing a live reverse migration is a risky operation that can lead to data loss.
Web and worker processes share code, but not the same context. In projects using Celery or Django-RQ, worker processes can continue processing tasks added to the queue by an older version of the code while the web component is already running a newer release. If you change a task or the format of the data it expects, you can break the asynchronous processing without affecting the web component. Therefore, it's important to treat task compatibility with the same care as migrations.
Scheduled tasks. If your application runs tasks at regular intervals—such as sending scheduled emails, cleaning temporary data, generating reports, or synchronizing with external services—you need to decide where and how they run within your deployment model. They can reside in a dedicated process managed by Kamal, be delegated to an external tool like a cron job on the host operating system, or run within existing worker processes using a scheduler like Celery Beat. Each option has implications for reliability, visibility, and debugging, and the important thing is to make a conscious decision rather than leaving it to chance or relying on habits inherited from the development phase.
Taken together, collectstatic, migrations, workers, and scheduled tasks are not minor details. They are part of the actual Django deployment and should be treated as such from the beginning, because as the project grows, this conceptual separation will be the foundation upon which any evolution of the architecture is built.
Prepare the servers and the base environment
Before the first deployment, the server must be ready to run the application stably and securely. This includes SSH access with keys, a deployment user with minimal permissions, Docker functioning correctly, and sufficient resources for the web component, workers, and auxiliary services.
It's also advisable to verify connectivity with the image registry and external application dependencies, such as PostgreSQL, Redis, or third-party APIs. If this connectivity fails, the problem lies not with Kamal, but with the environment.
The network must be intentionally defined. Only the necessary ports, typically 80 and 443, should be exposed, and the rest should be blocked by a firewall or the provider's network rules. On a VPS, this can be achieved with ufw or iptables; on AWS, with security groups. It's basic preparation, but it makes all the difference between a production-ready environment and one that only appears to be.
The first deployment and what it usually reveals
The initial deployment is the moment when the entire configuration ceases to be theoretical and faces the reality of the server, the network, and dependencies. Even if everything seems ready, it's normal for false assumptions to appear here that weren't present locally.
The sequence with Kamal is simple and predictable. The image is built from the Dockerfile, published to the registry, Kamal connects via SSH, downloads the image, starts the containers according to the defined roles, and executes the release hooks, such as collectstatic or migrations. This repeatability is precisely one of its advantages: deployment no longer depends on manual steps or the memory of a single person.
The most common issues the first time are missing environment variables, connectivity errors with PostgreSQL or Redis, system dependencies that weren't actually in the image, incorrect permissions on media files or logs, and problems with static files that only appear when accessing via the actual domain. These aren't unusual errors; they're a normal part of validating the environment.
None of these problems are serious if you anticipate them and diagnose them calmly. The first run isn't really a production launch, but rather a validation exercise that allows you to confirm that all the pieces fit together correctly. Treating it as such reduces frustration and allows you to resolve problems systematically. The goal isn't for everything to work perfectly the first time, but rather that when something goes wrong, you know exactly where to look to fix it.
Successive deployments, migrations and backtracking
Kamal's true value emerges after the second deployment. Each new release follows the same workflow: build the image, publish it, deploy it, and execute the defined hooks. This repeatability allows for more frequent deployments with less uncertainty because the process no longer relies on manual steps. Frequent deployments with incremental changes significantly reduce the failure rate in production because each individual release is easier to understand, verify, and revert if something isn't working as expected.
Migrations remain the most sensitive aspect. Simple migrations usually fit well into the normal release flow. However, destructive changes or those affecting large volumes of data should be handled separately and in phases, maintaining temporary compatibility between the code and the database schema. The reason is simple: an application rollback does not automatically reverse the migration. It requires more planning, but it avoids the scenario where a failed deployment leaves the database in an inconsistent state.
Rollback is useful, but not magic. Kamal allows you to quickly revert to a previous version of the code, but this doesn't undo changes to data, completed tasks, or applied migrations. Therefore, the rollback strategy should be planned before the problem arises, not during the incident. A well-designed zero-downtime deployment includes the ability to roll back as part of the normal plan, not as an emergency measure improvised when all else has failed.
Validations after deployment
There's a significant difference between "the release has started" and "the application is actually deployed and working correctly," and considering a deployment successful simply because the containers are running is a mistake that often results in issues discovered by users rather than the team. Verifying that everything works as expected after each deployment is just as important as the deployment itself, and these verifications should be defined beforehand to avoid relying on improvisation or individual memory.
At a minimum, you should validate after each deployment that the application is responding from the correct domain, that PostgreSQL and Redis are accessible, that static files and media files are still being served correctly, that at least one basic functional flow—such as login—works without errors, and that workers are processing tasks if asynchronous processes exist. It's also worthwhile to review the startup logs to detect recurring errors or silent failures.
If your team already has production monitoring configured, these validations integrate seamlessly with existing alerts and can be largely automated. If you don't yet have it, now is a great time to consider it, because every deployment represents a potential risk point that monitoring transforms into an observable and measurable control point.
Deployment as a consequence, not as an objective
Kamal is a great fit for deploying Django in a standardized way on your own servers, and it's especially useful for small and medium-sized teams that need a repeatable, predictable, and well-documented deployment process without the complexity of Kubernetes or a larger orchestration platform. Its value lies not in solving the entire production process, but in simplifying a specific and critical part—the act of deployment—and making it reliable enough to eliminate it as a source of stress.
For that to work, the application also needs to be well-designed: clear architecture, well-managed secrets, resolved persistence, compatible migrations, and validations after each release. If these pieces are missing, Kamal doesn't fix the problem; it only accelerates a process that remains fragile.
That's why its best fit is usually at that intermediate point where manual scripts are no longer sufficient, but complex orchestration still doesn't justify the cost of adopting and maintaining it. In many Django applications, this moment arrives sooner than you'd expect. Recognizing it early is what separates teams that deploy with confidence from those that keep crossing their fingers after every push.