Atlassian Forge provides a managed platform for building applications that extend Jira, Confluence, and other Atlassian products. Traditionally, developers use Forge functions to execute backend operations such as processing events, retrieving issue information, and handling user requests.
However, Forge functions are not suitable for every type of workload. Applications that require long-running computations, custom runtime environments, or specialized dependencies may need a different execution model.
Forge Container services address this requirement by allowing developers to package application code and dependencies into container images and run them directly on Atlassian-managed infrastructure.
This approach makes it possible to build services using different programming languages and frameworks while keeping the application within the Forge ecosystem.
In this article, we will explore how Forge Container services work, how to build a containerized Node.js application, configure its deployment, connect it to a Forge application, and manage its lifecycle in a production environment.
What Are Atlassian Forge Container Services?
Forge Container services allow developers to deploy containerized applications as part of their Forge apps.
Instead of executing every backend operation through a short-lived Forge function, developers can package a service into a Docker image and deploy it through the Forge platform.
The container runs on Atlassian-managed infrastructure, and Forge manages its deployment, scaling configuration, health checks, and lifecycle.
This is useful for applications that need:
- Long-running background processing.
- Custom programming languages and frameworks.
- Specialized system dependencies.
- Dedicated HTTP services.
- More control over application runtime behavior.
- Integration with external systems through supported network access.
For example, consider a Jira application that processes large project reports. A Forge function may be sufficient for retrieving issue information, but a separate containerized service can handle more intensive processing.
The Forge function and container service can work together rather than forcing the entire application into one execution environment.
Forge Container services are currently in Preview. They are fully supported for early adopters, including production use, but remain subject to changes and shorter deprecation windows.
Understanding the Forge Container Architecture
A Forge application can combine its existing modules with one or more containerized services.
A typical architecture looks like this:
Jira / Confluence
|
v
Forge Application
|
+------+------+
| |
v v
Forge Function UI Module
|
v
Container Service
|
v
Node.js / Java /
Python Application
|
v
External Services
or Forge APIs
Each component has a different responsibility.
The Forge frontend provides the user interface.
Forge functions handle application events and backend operations.
The container service performs operations that benefit from a dedicated runtime or longer execution time.
The containerized application exposes HTTP endpoints that can be invoked by supported Forge components.
One important architectural difference is that container services are stateless. They should not be used as permanent storage for customer data. Persistent information should be stored using Forge storage capabilities such as Forge KVS, Forge SQL, Custom Entities, or Object Store.
Forge Functions vs. Forge Container Services
Before implementing a container service, it is important to understand when it is actually needed.
| Feature | Forge Functions | Forge Container Services |
|---|
| Execution model | Managed function | Long-running container process |
| Runtime | Supported Forge runtimes | Custom container runtime |
| Programming languages | Supported Forge languages | Languages supported by the container image |
| Dependencies | Forge-compatible dependencies | Custom packaged dependencies |
| Persistent local storage | Not intended for permanent storage | Not supported |
| Long-running processing | Limited by function execution constraints | Suitable for longer-running workloads |
| Deployment | Forge application deployment | Image deployment through Forge |
| Health monitoring | Function invocation monitoring | Container health monitoring |
Forge functions remain the simpler choice for ordinary event handlers and lightweight backend operations.
Container services are more appropriate when an application needs a dedicated runtime, specialized dependencies, or longer-running processing.
Prerequisites for Building a Forge Container Service
Before starting, prepare the following:
- An Atlassian developer account.
- A Jira or Confluence development environment.
- Forge CLI version 12.0.0 or later.
- Docker installed on your development machine.
- Node.js installed for the example application.
- Basic knowledge of Dockerfiles and REST APIs.
- An existing Forge application or a new Forge project.
You should also understand the Forge manifest because container services are configured through the services section.
Step 1: Create a Node.js Service
Start by creating a directory for the container application.
mkdir jira-report-service
cd jira-report-service
npm init -y
npm install express
Create a file named server.js.
For this example, we will build a small HTTP service that generates a report for a Jira application.
const express = require("express");
const app = express();
app.use(express.json());
const PORT = process.env.SERVER_PORT || 8080;
app.get("/healthcheck", (req, res) => {
res.status(200).json({
status: "healthy"
});
});
app.post("/generate-report", async (req, res) => {
try {
const { projectKey, issues } = req.body;
if (!projectKey || !Array.isArray(issues)) {
return res.status(400).json({
error: "Invalid report input"
});
}
const report = {
projectKey,
totalIssues: issues.length,
completedIssues: issues.filter(
issue => issue.status === "Done"
).length,
generatedAt: new Date().toISOString()
};
return res.json(report);
} catch (error) {
console.error("Report generation failed", error);
return res.status(500).json({
error: "Unable to generate report"
});
}
});
const server = app.listen(PORT, "0.0.0.0", () => {
console.log(`Service listening on port ${PORT}`);
});
process.on("SIGTERM", () => {
console.log("Shutdown signal received");
server.close(() => {
console.log("HTTP server stopped");
process.exit(0);
});
});
The application contains two endpoints.
/healthcheck is used by Forge to verify that the container is responding.
/generate-report accepts project information and generates a summary.
The application reads the port from SERVER_PORT rather than assuming that a fixed port will always be used.
It also handles SIGTERM, allowing the application to close its HTTP server gracefully when Forge terminates or replaces the container.
Step 2: Create a Dockerfile
The next step is to package the application into a container image.
Create a file named Dockerfile.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
USER 1000:1000
EXPOSE 8080
CMD ["node", "server.js"]
There are several important configurations here.
The application uses an Alpine-based Node.js image to keep the runtime relatively small.
npm ci --omit=dev installs production dependencies using the lock file.
The Dockerfile creates a non-root user with UID and GID 1000. Forge Container services require containers to run using this identity.
The USER instruction ensures that the application does not execute as root.
The container should also be built for the Linux AMD64 architecture required by Forge Container services.
Step 3: Create a Forge Container Repository
Open your existing Forge application directory and create a container repository.
Run:
forge containers create
Provide a service name when prompted.
For example:
Service name: report-service
Forge creates a private container repository and returns its repository URI.
It follows this general structure:
forge-registry.services.atlassian.com/forge/APP_ID/report-service
Keep the returned URI because it will be required when building and uploading the image.
The service key must not exceed 19 characters.
Step 4: Build and Upload the Container Image
Generate a unique image tag.
For example, you can use a Git commit identifier:
export TAG=report-a1b2c3d4
Authenticate Docker with the Forge container registry:
forge containers docker-login
Build the image using the repository URI returned earlier.
docker build \
--platform linux/amd64 \
-t forge-registry.services.atlassian.com/forge/APP_ID/report-service:$TAG \
.
Upload the image:
docker push forge-registry.services.atlassian.com/forge/APP_ID/report-service:$TAG
Use the actual repository URI rather than the placeholder APP_ID.
Forge uses immutable image tags. Avoid relying on a moving latest tag. A Git commit hash or unique release identifier makes it easier to identify exactly which image is deployed.
Step 5: Configure the Container Service in manifest.yml
Open the Forge application's manifest.yml file.
Add the following configuration:
services:
- key: report-service
containers:
- key: report-container
tag: ${TAG}
resources:
cpu: "1"
memory: "2Gi"
health:
type: http
route:
path: /healthcheck
scaling:
min: 1
max: 1
modules:
endpoint:
- key: report-endpoint
service: report-service
route:
path: /generate-report
The configuration defines the containerized service and its endpoint.
key identifies the service.
tag selects the image version to deploy.
resources defines the CPU and memory allocation.
health specifies the HTTP endpoint used for health checks.
scaling defines the minimum and maximum service instances.
The endpoint module exposes the report-generation route to supported Forge application components.
The example uses one minimum and maximum instance. This is suitable for demonstrating the deployment configuration, but production capacity should be selected according to workload and scaling requirements.
Step 6: Deploy the Forge Application
Before deployment, make sure the image has been successfully uploaded and the manifest references the correct tag.
Deploy the application:
forge deploy
Forge launches the container service as part of the application deployment.
During deployment, Forge performs health checks before routing traffic to the new container.
The container must begin responding successfully within the startup health-check period. If it fails, Forge attempts a restart before failing the deployment.
Forge also uses a canary deployment strategy. This allows the platform to validate the new deployment before rolling it out.
When adding a services definition to an existing application for the first time, the deployment triggers a major app version upgrade.
Existing installations may therefore require an administrator-approved upgrade.
Step 7: Invoke the Container From a Forge Function
Once the service is deployed, a Forge backend function can invoke its endpoint using the invokeService capability.
For example:
import { invokeService } from "@forge/api";
export async function generateProjectReport(
projectKey,
issues
) {
const response = await invokeService(
"report-service",
{
path: "/generate-report",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: {
projectKey,
issues
}
}
);
if (!response.ok) {
throw new Error(
`Report service failed: ${response.status}`
);
}
return await response.json();
}
This example demonstrates the service invocation pattern.
The service key must match the key defined in the manifest.
The request contains the HTTP method, endpoint path, and JSON payload.
The backend checks the response status before returning the generated report.
For a production implementation, retrieve Jira issue information through authorized Forge APIs rather than accepting unrestricted issue data from the frontend.
Also configure the required Forge permissions and scopes for the application.
Handling Long-Running Operations
The main reason to consider container services is the ability to support workloads that do not fit comfortably within short-lived function execution.
Examples include:
- Generating large project reports.
- Processing complex documents.
- Performing computationally intensive transformations.
- Integrating with specialized libraries.
- Running dedicated application services.
However, a long-running HTTP request is not always the best architecture.
For operations that may take several minutes, consider accepting a request, creating a background job, and returning a job identifier.
The application can then expose a separate endpoint for checking job status.
Because containers are stateless and can be terminated or replaced, job state must be stored in durable storage rather than local memory or the container filesystem.
For example:
Forge Function
|
v
Create Background Job
|
v
Persistent Job Storage
|
v
Container Service
|
v
Process Job
|
v
Update Job Status
|
v
Forge UI Retrieves Result
This design makes processing more reliable when containers restart or deployments take place.
Monitoring and Troubleshooting Container Services
Forge provides built-in observability for containerized applications.
Developers can monitor:
- CPU utilization.
- Memory utilization.
- Service availability.
- Container instance count.
- Invocation response times.
- Container logs.
You can inspect container logs using:
forge logs --containers
The developer console also provides service health and invocation metrics.
Common problems include:
| Problem | Possible cause | Recommended solution |
|---|
| Container deployment fails | Incorrect image architecture | Build for Linux AMD64 |
| Application does not start | Wrong startup command | Check Dockerfile and runtime logs |
| Health check fails | Incorrect endpoint or port | Verify health route and SERVER_PORT |
| Permission denied | Container runs as root or wrong user | Use UID and GID 1000 |
| Image upload fails | Registry authentication issue | Run forge containers docker-login |
| Service cannot be reached | Endpoint configuration mismatch | Check service key and route |
| External API request fails | Egress proxy not configured | Use FORGE_EGRESS_PROXY_URL |
Best Practices for Production Forge Containers
- Use non-root execution. Run the application with UID and GID 1000.
- Keep containers stateless. Store persistent information in supported Forge storage services.
- Use immutable image tags. Identify every deployment with a unique release tag.
- Implement health endpoints. Make sure the health check does not depend on an unrelated external service.
- Handle SIGTERM. Stop accepting new requests and finish or safely abandon active work.
- Use the assigned SERVER_PORT. Do not assume that the application will always receive traffic on port 8080.
- Route outbound traffic through the configured Forge egress proxy.
- Protect customer data. Scope every request according to its Forge installation.
- Rebuild images regularly. Images older than 180 days are considered stale and are blocked from production deployment under the current image security requirements.
- Monitor CPU, memory, and invocation latency to identify resource constraints.
- Avoid destructive startup operations. Canary deployments can run new and existing instances alongside each other.
- Review Preview limitations and service pricing before adopting containers across multiple installations.
Advantages and Disadvantages of Forge Container Services
Advantages
- Supports custom programming languages and frameworks.
- Enables longer-running workloads compared with traditional Forge functions.
- Provides container lifecycle management through Forge.
- Uses Atlassian-managed infrastructure.
- Supports integration with Forge functions, events, and frontend components.
- Provides built-in health checks and observability.
- Can help applications retain eligibility for the Runs on Atlassian badge when all other requirements are met.
Disadvantages
- Requires Docker image development and maintenance.
- Containers are stateless and cannot provide persistent local storage.
- Image security and architecture requirements must be followed.
- Container services introduce additional deployment and operational costs.
- Preview limitations restrict some management operations.
- Adding container services to an existing app triggers a major version upgrade.
- Containers cannot be accessed through SSH or other remote login methods.
Summary
Atlassian Forge Container services provide a way to run containerized applications directly on Atlassian-managed infrastructure.
They are useful when a Forge application needs custom dependencies, dedicated HTTP services, or longer-running processing that does not fit traditional Forge functions.
By creating a Docker image, uploading it to a Forge-managed repository, configuring the services section in the manifest, and deploying the application, developers can introduce container-based workloads into their Forge architecture.
However, successful production deployment requires careful attention to stateless design, non-root execution, health checks, immutable image tags, tenant isolation, and graceful shutdown.
Forge functions remain appropriate for lightweight application operations, while container services provide an additional execution option for more demanding workloads. Choosing the right combination helps developers build applications that are easier to maintain and better suited to their actual processing requirements.
Join the conversation! Your thoughts help the community grow.