Kubernetes Made Simple: A Guide for JVM Developers

This article was written by an external contributor.

Michael Nyamande

A digital product manager by day, Michael is a tech enthusiast who is always tinkering with different technologies. His interests include web and mobile frameworks, NoCode development, and blockchain development.

Michael on social media

Kubernetes is a container orchestration system for deploying, scaling, and managing containerized applications. If you build services on the Java virtual machine (JVM), you likely know that most microservices run on Kubernetes. Kubernetes has become the de facto standard for running containerized microservices at scale. However, Kubernetes is famously complex, with many new concepts (Pods, Deployments, Services, etc.) to master, and thus, has a steep learning curve.

This tutorial helps ease that complexity for JVM developers. It focuses on what you need to ship a Kotlin or Java Spring Boot app to a cluster, step-by-step, with simple explanations and runnable examples.

You’ll learn the basics of Kubernetes by deploying a Kotlin Spring Boot application onto a Kubernetes cluster. You’ll also cover what deployment and services are, how to manage configurations using ConfigMaps and Secrets, and what the best practices are for running JVM applications on Kubernetes.

Prerequisites
Before diving in, make sure you have the following:

  • Docker: Install and run this locally. Docker builds a container image of your app.
  • Kubernetes: Install a Kubernetes environment. For this tutorial, you’ll use Minikube, a local single-node cluster, and the kubectl CLI for interacting with the cluster. You can download Minikube on their official site, and it comes bundled with kubectl.
  • Docker registry: Create, for example, a Docker Hub account to push and pull your image. You can also use Minikube’s local Docker registry.

Set Up the Sample Kotlin Spring Boot App (Optional)

While you can use an existing Kotlin Spring Boot application for this tutorial, if you want to follow along with the code used here, you can create a new project with Spring Initializr. If you’re using an existing Spring Boot application, you can jump directly to the next section.

Select Kotlin as your language and Java 21 as our runtime. Make sure to add Spring Web, Spring Data JPA, and H2 as dependencies. You’ll use Spring Web to create REST endpoints, Spring Data JPA to connect to a PostgreSQL database, and H2 (an in-memory database) to test the database logic locally:

After creating and downloading the project, locate your main application file. If you used Spring Initializr, the file will be named after your application with Application.kt appended— for example, a project named Demo will have a file called DemoApplication.kt. Add the following code to create a @RestController that returns Hello World, which will let you verify the deployment is working:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

@RestController
class HelloController {
    @GetMapping("/hello")
    fun hello(): String = "Hello World"
}

The entire Spring Boot app and REST controller fit in just a few lines, thanks to Kotlin features like type inference and single-expression functions.

Containerize a JVM App Using Docker

To deploy your application to Kubernetes, you need to initially package it into a container image using Docker. Create a Dockerfile in the project root:

FROM openjdk:21-jdk-slim

WORKDIR /app

# Copy the JAR file from builder stage
COPY build/libs/*-SNAPSHOT.jar app.jar

# Expose port 8080
EXPOSE 8080

# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

This Dockerfile uses a lightweight Java 21 base image, copies in the built JAR file, and runs it. Kotlin and Java interoperability means the Spring Boot JAR runs just like any Java app in the container.

To build the image, you initially need to create a JAR file with gradle build or mvn clean package, depending on which build manager you’re using. If using Maven, update the Dockerfile to use target/*.jar instead of build/libs/*-SNAPSHOT.jar.

After that, build the image:

docker build -t kotlin-app:latest .

Before you can push the image to Docker Hub, you need to execute this command to log in:

docker login

Note that you may be prompted to enter your Docker Hub credentials to complete the login step.

Next, push the image to Docker Hub or another registry so your Kubernetes cluster can access it:

docker tag kotlin-app:latest YOUR_DOCKERHUB_USER/kotlin-app:latest
docker push YOUR_DOCKERHUB_USER/kotlin-app:latest

Deploy the Application to a Kubernetes Cluster

To run your application on Kubernetes, you need to tell Kubernetes how to configure and run it. You do this using manifest files, which are typically written in YAML. These files declaratively define the desired state of your application in the cluster. For a basic deployment, you need two key Kubernetes objects: a Deployment manifest and a Service manifest.

Add the Deployment Manifest

A Deployment manages replicated Pods and handles rolling updates. A Pod is Kubernetes’s smallest unit that runs your container. Deployments ensure your specified number of Pods stay running and update them safely without downtime.

Create a file named k8s/deployment.yaml that defines your Deployment so that Kubernetes can run the application:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kotlin-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kotlin-app
  template:
    metadata:
      labels:
        app: kotlin-app
    spec:
      containers:
      - name: kotlin-k8s-app
        image: <your-username>/kotlin-app:latest
        ports:
        - containerPort: 8080

The manifest above makes these declarations:

  • kind: Deployment specifies the type of object.
  • spec.replicas: 1 tells Kubernetes how many instances of the application you want running.
  • spec.selector.matchLabels is how the Deployment knows which Pods to manage. It looks for Pods with the label app: kotlin-app.
  • spec.template is the blueprint for the Pods. It defines the container(s) to run inside the Pod.
  • spec.containers.image specifies the Docker image to pull.
  • spec.containers.ports.containerPort informs Kubernetes that your application inside the container listens on port 8080.

Include the Service Manifest

While a Deployment ensures your Pods are running, those Pods are ephemeral; each time they restart, they get a new internal IP address. A Service solves this by acting as a stable entry point with a fixed IP and DNS name, automatically routing traffic to the Pods identified by its label selector. This guarantees that even if Pods restart or change IPs, traffic still reaches the intended application.

Create a file named service.yaml in the k8s folder:

apiVersion: v1
kind: Service
metadata:
  name: kotlin-app-service
spec:
  selector:
    app: kotlin-app
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
  type: NodePort

The manifest defines:

  • kind: Service specifies the object type.
  • spec.selector must match the labels of the Pods (app: kotlin-app). This is how the Service knows where to send traffic.
  • spec.ports maps the Service’s port (port: 8080) to the container’s port (targetPort: 8080).
  • spec.type: NodePort exposes the application on a static port on each node in the cluster, making it accessible for local development with Minikube. In a cloud environment, you typically use a LoadBalancer.

Deploy to a Cluster Using Minikube

To deploy this to a cluster, run Minikube with minikube start and apply the manifests using the following commands:

kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

After applying, you can verify that everything is running using kubectl get pods. You should then get a result like this:

NAME                                    READY   STATUS    RESTARTS      AGE
kotlin-app-deployment-744476956-bfwg4   1/1     Running   0             20s

To access your application, run minikube service kotlin-app-service. This command finds the service in Minikube and opens a URL in your host browser via port forwarding. The output shows an IP and port (eg http://192.168.49.2:30000). Visiting http://<minikube-ip>:30000/hello should call your Spring app and return the Hello World message.

Extend the Kotlin App with ConfigMap

Hard-coding configuration in images forces rebuilds for simple changes and risks exposing sensitive data. Kubernetes provides ConfigMaps for non-sensitive configuration and Secrets for sensitive data, like passwords.

To demonstrate ConfigMaps, replace the hard-coded greeting with one that can be set through a configuration.

To do this, update the controller to read the message from an environment variable:

@RestController
class HelloController {
    @Value("${greeting.message:Hello}")
    lateinit var greetingMsg: String

    @GetMapping("/hello")
    fun hello(): String = greetingMsg
}

This code snippet declares a variable greetingMsg, which pulls a value from the environment or defaults to "Hello" if it doesn’t find the specific environment variable.

Now, create a configmap.yaml file in the k8s folder; this sets the greeting configuration so you can change it without rebuilding the image:

apiVersion: v1
kind: ConfigMap
metadata:
  name: kotlin-app-config
data:
  application.properties: |
    greeting.message=Hello from a ConfigMap!

To use this ConfigMap, you need to mount it as a volume into the Pod. This approach prevents configuration values from being accidentally logged in process lists and allows for configuration updates without restarting the Pod.

Additionally, ConfigMaps can store larger configuration files and support multiple configuration formats.

Update your k8s/deployment.yaml so that it uses the new ConfigMap that you defined earlier:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kotlin-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kotlin-app
  template:
    metadata:
      labels:
        app: kotlin-app
    spec:
      containers:
      - name: kotlin-k8s-app
        image: <your-username>/kotlin-app:v2
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: config-volume
          mountPath: /app/config
      volumes:
      - name: config-volume
        configMap:
          name: kotlin-app-config

This manifest adds a volumes section that defines a volume named config-volume, which sources its data from the kotlin-app-config ConfigMap. It also adds a volumeMounts entry to the container specification, mounting this volume at /app/config. This setup allows Spring Boot to automatically detect and load the application.properties file from the /config directory, making it easy to manage configuration through Kubernetes.

The deployment also updates the image to an updated Docker image. Let’s create this image by rebuilding the application and creating a new Docker image with an updated tag (eg v2). Then push it to your registry so Kubernetes can pull the latest version:

# 1) Rebuild the Kotlin app 
./gradlew clean build            # Gradle
# or
mvn clean package                # Maven

# 2) Build a new Docker image with a new tag (v2)
docker build -t <your-username>/kotlin-app:v2 .

# 3) Push the image so the cluster can pull it (skip if using Minikube's Docker daemon)
docker push <your-username>/kotlin-app:v2

After pushing the new image, apply the new configmap.yaml and the updated deployment.yaml:

kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/deployment.yaml

Connect to a Database

When your application needs to store and retrieve data, such as user accounts or business records, you need to manage persistent storage alongside your deployments. Kubernetes lets you run databases like PostgreSQL as managed Deployments, using persistent volumes for data durability and Secrets for credentials.

Let’s walk through deploying PostgreSQL and connecting it to your application.

To keep your credentials out of the container and enable safe injection into Pods, you need to define a Secret. A Kubernetes Secret is like a ConfigMap but intended for confidential info (passwords, tokens). Create postgres-secret.yaml to safely store the database credentials:

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
stringData:
  POSTGRES_USER: "postgres"
  POSTGRES_PASSWORD: "mysecretpassword"
  POSTGRES_DB: "greetingsdb"

Note: stringData is used for convenience; Kubernetes stores this as a Base64-encoded value.

Create deployments/postgres.yaml to run PostgreSQL and expose it with a stable DNS name:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: POSTGRES_DB
        - name: POSTGRES_USER
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: POSTGRES_USER
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: POSTGRES_PASSWORD
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: postgres-storage
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
  type: NodePort

This manifest does two things:

  • Creates a Service named postgres-service so your application can connect to the database using a stable DNS name.
  • Creates a Deployment that runs PostgreSQL, using the Secret for the password. It mounts /var/lib/postgresql/data to a volume (here, an emptyDir for simplicity). In production, you’d use a StatefulSet and a PersistentVolumeClaim to ensure data persists across Pod restarts and node failures.

Let’s also update the Kotlin app to connect to a PostgreSQL database. In this example, the app returns a custom greeting with the user’s details that it pulls from the database. You can use Spring Data JPA.

To connect to a PostgreSQL database and use JPA, you need to add the PostgreSQL Java Database Connectivity (JDBC) driver. The PostgreSQL driver is essential because it allows your application to communicate with the database running in Kubernetes. Add this to the dependencies block in your build.gradle.kts (or pom.xml if you’re using Maven) so it’s available at compile and runtime:

implementation("org.postgresql:postgresql")

For local development, the application uses H2 (which you added earlier as a dependency) as a lightweight option for testing without having to spin up a full PostgreSQL instance. The application interacts only with PostgreSQL when deployed to the Kubernetes cluster.

Create a file and name it User.kt. Use this to model the users table. Additionally, create a Spring Data JPA repository for database lookups:

import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Entity
@Table(name = "users")
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    val name: String = "",

    @Column(nullable = false)
    val email: String = ""
)

@Repository
interface UserRepository : JpaRepository<User, Long> {
    fun findByName(name: String): User?
}

This snippet uses a Kotlin class to define a User entity. With Kotlin’s primary constructor syntax, you can declare mutable properties and initialize the object in a single definition, eliminating the need for boilerplate getters and setters required in Java entities. The snippet also defines a UserRepository that handles retrieving user details from the database.

Update the main controller with this GetMapping to return dynamic greetings based on username:

import org.springframework.web.bind.annotation.PathVariable

@RestController
class HelloController(private val userRepository: UserRepository) {

    @GetMapping("/hello")
    fun hello(): String = "Hello World"

    @GetMapping( "/hello/{name}")
    fun getGreeting( @PathVariable name: String = "world"): String =
        userRepository.findByName(name)
            ?.let { "Hello ${it.name}! Your email is ${it.email}." 
            ?: "Hello $name! (User not found in database)"
}

This code injects the UserRepository into the Controller, allowing you to use it in the getGreeting method. This method returns the user’s name, along with their email, if the user exists in the database; otherwise, it outputs that the user wasn’t found. It uses Kotlin null safety features to produce a response without unsafe casts or a NullPointerException.

Next, update the src/main/resources/application.properties file with the PostgreSQL configuration:

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

The properties file configures Spring Data JPA settings. The hibernate.ddl-auto=update property enables automatic schema updates based on the @Entity definitions. This ensures that the User table is created at runtime if it doesn’t exist in the database. The spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect tells hibernate to use PostgreSQL-specific queries.

To use the updated code, rebuild the application and Docker image with the changes, and update the Deployment to include the new environment variables as Secrets:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kotlin-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kotlin-app
  template:
    metadata:
      labels:
        app: kotlin-app
    spec:
      containers:
      - name: kotlin-k8s-app
        image: <your-username>/kotlin-app:v3
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_DATASOURCE_URL
          value: "jdbc:postgresql://postgres-service:5432/greetingsdb"
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: POSTGRES_USER
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: POSTGRES_PASSWORD

The configuration now contains a new env section that defines the database URL, username, and password from the Secrets definition. Spring uses these variables to connect to the database.

Apply the new manifests using this command:

kubectl apply -f k8s/postgres-secret.yaml
kubectl apply -f k8s/postgres.yaml
kubectl apply -f k8s/deployment.yaml

You can use minikube service kotlin-app-service to expose the application using an external IP address and navigate to <url>/hello/<username> to test. If the username doesn’t exist in the User table of the PostgreSQL database, you’ll get this output:

Hello <username>! (User not found in database)

Dynamic routing using Ingress

Sometimes you might want to roll out new features to a subset of users to test out how they work before a full production release, for example, during beta testing. To do this, you can have route traffic from your Kubernetes cluster to different services depending on certain rules. This is done via an Ingress. An Ingress sits at the edge of the cluster and routes HTTP traffic to Services based on rules like host, path, or headers.

In this example, you’ll route normal traffic to v2 of the application and route all traffic with a special header to the new v3 image. This allows you to test a new database feature on a subset of users or clients before a full, stable rollout.

To enable the NGINX Ingress controller in Minikube:

minikube addons enable ingress

Create a new v2-application file that contains the deployment and Service for the v2 version of the app, save it to k8s/v2-app.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kotlin-app-v2-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kotlin-app-v2
  template:
    metadata:
      labels:
        app: kotlin-app-v2
    spec:
      containers:
        - name: kotlin-k8s-app-v2
          image: <your-username>/kotlin-app:v2
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: config-volume
              mountPath: /app/config
      volumes:
        - name: config-volume
          configMap:
            name: kotlin-app-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kotlin-app-config
data:
  application.properties: |
    greeting.message=Hello from a v2 stable app!
---
apiVersion: v1
kind: Service
metadata:
  name: kotlin-app-v2-service
spec:
  selector:
    app: kotlin-app-v2
  ports:
    - port: 8080
      targetPort: 8080
  type: ClusterIP

The example above is similar to the Deployment and Service you set up earlier, except the Service type is now ClusterIP instead of NodePort. ClusterIP only exposes the Service within the cluster, making it accessible to other Pods but not directly from outside the cluster. In contrast, NodePort exposes the Service on a static port on each node’s IP, allowing external access. Since the Ingress handles external traffic routing, you use ClusterIP for internal communication between the Ingress and your Services.

With Services in place, you can add the Ingress resources. Create a new ingress file to receive traffic and direct it to the v2 version of your service, and save it as k8s/ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kotlin-app
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kotlin-app-v2-service
            port:
              number: 8080

To direct traffic to the v3 version of the application, you can utilize the canary annotations of the ingress controller. Create another ingress definition file and save it to k8s/ingress-canary.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kotlin-app-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "X-Client-Version"
    nginx.ingress.kubernetes.io/canary-by-header-value: "v3"
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kotlin-app-service
            port:
              number: 8080

The canary Ingress above uses NGINX’s canary annotations to implement header-based routing. When a request includes the header X-Client-Version: v3, the Ingress controller routes it to the kotlin-app-service (your v3 Pods). All other requests without this header go to kotlin-app-v2-service (your stable v2 Pods). This pattern lets you safely test new features in production with a subset of users, such as internal testers or beta users, while the majority of traffic continues to hit the stable version.

The canary: "true" annotation tells NGINX this Ingress is a canary rule, and the canary-by-header annotations define the matching logic.

Apply the new manifests using the following commands:

kubectl apply -f k8s/v2-app.yaml
kubectl apply -f k8s/ingress.yaml
kubectl apply -f k8s/ingress-canary.yaml

To test this out, run minikube tunnel to tunnel your minikube instance and make it available on localhost. To view our application you simply need to navigate to http://localhost/hello.

You can verify the routing behavior with curl. A request without the header goes to v2:

curl  http://127.0.0.1/hello 

This returns “Hello from a v2 stable app!”.

Running the same request with the X-Client-Version header, returns a response from v3 of the application:

$ curl -H "X-Client-Version:v3" http://127.0.0.1/hello
Hello World

You can also run the same on with /hello/{name} to verify it routes to v3 of the application:

curl -H "X-Client-Version:v3" http://127.0.0.1/hello/mike
Hello mike! (User not found in database)

You can find the tutorial’s full codebase on this GitHub repository. Switch between different branches to access different parts of the tutorial.

Follow These Best Practices

When deploying JVM-based microservices on Kubernetes, keep these practices in mind:

Configure Health Checks (Liveness and Readiness Probes)

Kubernetes needs to know if your application is healthy and ready to serve traffic. Health checks let Kubernetes direct traffic to healthy Pods and restart failing ones. Spring Boot Actuator provides /actuator/health/liveness and /actuator/health/readiness endpoints. Kubernetes sends HTTP requests to these endpoints, and non-2xx responses trigger container restarts.

Use ConfigMap and Secret Manifests

Do not hard-code environment-specific or sensitive data into your image. As you learned in this tutorial, it’s best to store non-sensitive configs (like feature flags, greeting messages) in ConfigMaps and more confidential data (passwords, tokens) in Secrets. This makes it easy to change settings without rebuilding containers.

Set CPU/Memory Resource Limits

Kubernetes allows you to set memory and cpu requests and limits. This prevents your app from consuming unlimited resources and impacting other pods. Without limits, a runaway JVM can crash your entire node or be killed unexpectedly, so proper limits ensure cluster stability and cost control.

Conclusion

This tutorial showed you how to containerize and deploy a Kotlin Spring Boot application on Kubernetes. Along the way, you learned important Kubernetes fundamentals, like Pods, Deployments, Services, ConfigMaps, and Secrets.

You also saw why Kotlin is a good choice for server-side development, especially with Spring, because of Kotlin features, like null safety, data classes, and coroutines. If you use Java, you can introduce Kotlin gradually into existing Spring projects without rewriting your stack. Explore more on Kotlin for server-side development on their official landing page.

Junie Now Integrated Into the AI Chat

At JetBrains, we’re always listening closely to our users, ensuring that the products we ship provide a consistent and intuitive user experience. One area for improvement that we both noticed is the presence of two separate UIs – one for Junie, the coding agent by JetBrains, and one for the JetBrains AI chat.

We’ve now taken the first step toward uniting the two interfaces, making Junie a first-class JetBrains AI citizen. Starting today, this functionality is available in Beta, meaning you can select Junie right from the AI chat and start using it there.

Our intention is to eventually deprecate and fully merge the separate Junie interface into the AI chat, leaving a single space for you to use Junie and other AI functionality. Your feedback during this stage is especially important! Try the new unified experience and let us know what you think – your input will help us make sure it best serves your needs.

We expect the user experience provided by the current Beta version of Junie inside the AI chat to be on par with that of the plugin version. However, you can expect more improvements in the future.

Why did we have two separate UIs in the first place?

When coding agents first appeared, we explored multiple R&D directions in parallel to identify the most effective IDE experience. That exploration led us to develop two chat interfaces, each testing a different approach.

With the industry now converging on clearer UX standards for coding agents, we’re unifying these two interfaces into a single, consistent one. It’s a natural evolution that allows us to streamline the Junie experience without compromising on innovation.

Transition period

The standalone Junie plugin will remain available to help you adjust to the change at your own pace. Your existing settings – active models, Action Allowlist, MCP server, and more – will be retained and gradually merged into the unified interface. In the first iteration, Junie and the AI chat will use separate settings – changes in the AI chat won’t affect Junie, while updates in Junie will apply to both the standalone plugin and the chat version. We’ll notify you in advance of any further changes to come and do our best to make the transition as smooth as possible.

How to try it

  1. Open the AI chat in your IDE.
  2. Choose Junie from the agent selector.
  3. Run a prompt – the agent will then download and be installed automatically.

If you haven’t installed the AI chat yet, do so by following the steps in this installation guide.

What’s next?

We’ll continue to integrate Junie’s features into the unified interface and refine the UX to ensure that AI tools in JetBrains IDEs work seamlessly together.

Your feedback matters! Tell us which AI use cases are most important for you and what you’d like to see next by submitting a feature request. This helps us ensure our priorities are aligned with those of our users.

For hardcore Junie fans, our Discord is still open. Join it to get all the latest updates, discuss your experiences, and share your feedback with the Junie team and community.

Drone Girl’s 2025 Holiday Gift Guide

Whether you’re shopping for an experienced pilot or someone who’s never flown before, I’ve put together the ultimate gift guide for drone enthusiasts. From budget-friendly starters to premium FPV setups, here’s everything you need to know to nail your holiday shopping this year.

Drone gifts for the absolute beginner

The DJI Neo drone. (Photo by Sally French)

DJI Neo – $199

At just $199, the DJI Neo is my top pick for anyone getting started with drones (there’s also the DJI Neo 2, but it’s harder to get your hands on and it’s more expensive). It launches from the palm of your hand, shoots impressive 4K video and weighs only 135 grams (no FAA registration needed!). The fully-enclosed propellers make it safe for flying indoors or around kids. Perfect for content creators, vloggers or anyone who wants to capture unique selfie angles without the learning curve of a traditional controller.

Why I love it: It’s the most beginner-friendly drone on the market, and you can’t beat the price.

DJI Mini 4K – Buy on Amazon

For just $100 more than the Neo, the DJI Mini 4K delivers significantly more capability. You get proper 4K video at 30fps, a dedicated remote controller (included!), and 31 minutes of flight time—nearly double the Neo. It weighs under 250 grams, so recreational pilots still don’t need to register it. This is my recommendation if you want a “real” drone experience without breaking the bank.

Why I love it: Best value for the price, hands down. It’s what I recommend when people ask for the cheapest drone that still delivers quality results.

For the drone pilot ready to upgrade

Sally French, The Drone Girl, reviews the DJI Flip. (Photo by Hamilton Nguyen)

DJI Flip – $439

The DJI Flip is the drone that surprised me most this year. At $439, it delivers the same 48MP camera quality as the Mini 4 Pro (which costs $320 more!) but in a unique design with foldable, full-coverage propeller guards. It’s perfect for travelers, vloggers and content creators who prioritize portability and safety. The vertical shooting mode is clutch for Instagram Reels and TikTok.

Why I love it: Professional-level camera in a beginner-friendly package. The propeller guards mean I don’t stress about flying indoors or around people.

DJI Mini 3$419

If battery life matters more than anything else, the Mini 3 is your drone. You get up to 38 minutes of flight time (or 51 minutes with the Plus battery), plus a large 1/1.3-inch sensor that captures stunning 4K HDR video. The True Vertical Shooting feature makes it perfect for social media content. It’s recently dropped in price, making it one of the best values in the under-$500 category.

Why I love it: Longest flight time of any DJI drone under $500. When you need extra airtime for complex shots, this is the one.

HOVERAir X1 – $439

The HoverAir X1 stands out as the best non-DJI option in this price range. It’s ultra-compact (128g), launches from your palm, and has a unique party trick: it records audio through your phone. The fully-enclosed propellers and pre-programmed flight paths make it dead simple to use. While the 2.7K video isn’t as sharp as DJI’s offerings, it’s perfect for casual creators who value portability above all else.

Why I love it: Sometimes you want a DJI alternative, and this is the best one under $500.

For the FPV adventurer

Sally French, The Drone Girl, reviews the DJI Avata 2 wearing the DJI Goggles 3 and using the DJI RC Motion 3. (Photo by Sally French)

DJI Avata 2 – $999

The DJI Avata 2 is hands-down the best indoor drone and the most thrilling drone I’ve flown this year. With built-in propeller guards, you can zip through tight spaces without worrying about crashes. The included DJI Goggles 3 and RC Motion 3 controller deliver an immersive FPV experience that feels like you’re in the pilot’s seat. The upgraded 1/1.3-inch sensor shoots gorgeous 4K video, and 23 minutes of flight time gives you plenty of room to nail your shots.

Why I love it: It makes FPV flying accessible to everyone. You get that adrenaline rush without needing to build your own racing drone.

DroneMask 2$179

Want to turn your existing drone into an FPV experience without spending $500 on DJI Goggles? The DroneMask 2 is your answer. This clever headset works with your smartphone and almost any camera drone to give you that first-person view. At less than half the price of DJI Goggles 3, it’s perfect for anyone FPV-curious who wants to dip their toes in without committing to a full Avata setup.

Why I love it: Most affordable way to experience FPV flying with a drone you already own.

For cameras that aren’t actually drones

DJI Osmo Action 4 – Price varies

Every drone pilot needs a great action camera in addition to their drone. We are now onto the Osmo Action 6 — but my Osmo Action 4 is still holding strong and remains my go-to for ground-level action shots that complement aerial footage. It shoots 4K/120fps, handles extreme temperatures and the magnetic mounting system is genius. Plus, it plays nice with DJI drones in your workflow.

Why I love it: It’s the perfect companion to drone footage. The color science matches DJI drones, making editing a breeze.

Insta360 X5 Essentials Bundle – Buy on Amazon

Yes, I’m hawking a non-DJI camera right here! The Insta360 X5 takes 360-degree footage to a whole new level. Just look to the work we see from drone pilots like Eric Thurber that don’t rely just on drones.

Gifts that don’t take up any space

Drone Adventurer Masterclass

Enroll now – Use code SALLY50 for a discount

Whether you want to shoot real estate, weddings, or commercial work, this masterclass covers it all. The investment pays for itself after your first paid gig.

UAV Coach Courses

The gold standard for Part 107 test prep. If your gift recipient is serious about flying commercially, they need this training. UAV Coach consistently updates their material to match current FAA regulations.

Drone Dojo Subscription

Ongoing education is critical in this industry. Drone Dojo keeps pilots current on regulations, techniques, and industry trends with fresh content every month.

Masterclass Subscription

Beyond just drones, Masterclass offers incredible courses from the world’s best photographers, filmmakers, and storytellers. I’ve learned so much about composition and storytelling from famous experts in the field such as Jimmy Chin, the guy who made “Free Solo.”

Essential Accessories

Premium Drone Supplies Custom Landing Pad

A quality landing pad isn’t just about protecting your drone, it’s about looking professional. For hobbyists, maybe it’s just about having fun and creativie with your shoot. That’s where custom drone landing pads come into the picture.

I use Premium Drone Supplies because they offer custom options that let you add your logo (or hey, maybe your favorite picture.

Save $5 on your order from Premium Drone Supplies with code SALLYFRENCH.

Why I love it: Durable, folds down small, and the bright colors make it easy to spot in any terrain.

Spare batteries and propellers – Price varies

This is the 2025 holiday gift for the practical pilot. Extra batteries mean more flight time, and spare propellers mean your recipient won’t be grounded after their first crash (and trust me, there will be crashes). Make sure you match the battery to their specific drone model. Not sure what drone your loved one has? Just ask (because propellers and batteries can be incompatible with different drone models).

MicroSD Cards – Price varies

You can never have too many microSD cards. I recommend SanDisk Extreme cards as a budget pick — they’re fast enough for 4K video and reliable enough that I trust them with paid gigs. Get at least 128GB.

Pro tip: Label them!

Apple AirTags – $30

I lose a lot of things! With drones, there’s the elevated risk of theft. AirTags can offer some peace of mind that at least you’ll know where your stuff is at. I recommend tucking one in your drone case.

Belkin BoostCharge Plus 10K Power Bank$55

A portable battery pack is essential for all-day flying sessions. This Belkin model is my favorite—it charges fast, holds enough juice for multiple drone batteries and the compact size means it actually fits in my camera bag.

Fun and unique gifts for drone pilots

Santa Drone Ornament – $16

Every drone pilot needs a drone ornament on their tree. Better yet when it’s Santa flying a DJI Phantom lookalike. It’s the little touches that show you get their passion.

Personalized Puzzle – $10

Turn their favorite drone shot into a custom puzzle. It’s a thoughtful gift that celebrates their best aerial work.

TinyCircuits TinyTV 2 – $60

This tiny retro TV displays videos and photos from a microSD card. Load it up with their best drone footage, and they’ve got the world’s smallest highlight reel on their desk. It’s quirky, nostalgic, and actually useful.

National Geographic World From Above$26.56 (41% off)

This stunning coffee table book is basically 200+ pages of aerial photography porn. Curated by National Geographic explorer Jeffrey Kerby, it showcases the world from above through the lenses of the world’s most innovative photographers—many of whom used drones to capture these shots.

What makes it special:

  • Over 200 full-color aerial images organized by color, shape, texture, and scale
  • In-depth interviews with photographers about how they captured their most impressive shots
  • Sidebars exploring the history of aerial perspective, from ancient earthworks to modern drones
  • Hardcover, oversized format (perfect for that coffee table or office)

Perfect for: Drone pilots who need inspiration, photographers who appreciate composition from unique angles, or anyone who loves stunning nature photography.

Aura Frames – $180

This is digital photo frame that ‘s honestly a great gift for anyone who loves vacation slides and family photos. It rotates through your curated list of photos. For drone pilots, it makes their best drone shots visible every day.

2025 holiday gift tips from a drone expert

Match the gift to the skill level. A $1,000 FPV drone is wasted on someone who’s never flown before, just like a toy drone will disappoint an experienced pilot.

When in doubt, go with accessories. Extra batteries, a landing pad, or training courses are always appreciated and never the wrong choice.

Check compatibility. DJI batteries and accessories are model-specific. Double-check you’re buying for the right drone before you wrap it.

Consider an experience. Sometimes the best gift is a drone photography workshop, a guided flying lesson, or even a day trip somewhere scenic to fly together.

Gift cards work. If you’re not sure what they need, a gift card to B&H Photo, Amazon, or directly from DJI means they can get exactly what they want.

Happy holidays, and happy flying! 🎄✈

— Sally French, The Drone Girl


Disclosure: Some links in this guide are affiliate links, which means I may earn a small commission if you make a purchase through them. This doesn’t cost you anything extra and helps support my work creating free content like this gift guide. I only recommend products I’ve personally tested and genuinely believe in. Thank you for supporting The Drone Girl!

The post Drone Girl’s 2025 Holiday Gift Guide appeared first on The Drone Girl.