How to insert multiple images into a single post within a CPT

As I understand it, you will need create a meta box with fields and use jQuery to add/remove the image boxes. You can also use jQuery UI to make the fields draggable if you like.

Your meta box code will look something like this

// Fields
$prefix = 'your_prefix_';

$custom_meta_fields = array(
array(
   'label' => 'Gallery Images',
   'desc' => 'Add additional images for this portfolio item.',
   'id' => $prefix.'gallery_images',
   'scope' => array('your_custom_post_type'),
   'type' => 'repeatable_image',
),
);

// Add the Meta Box
function add_custom_meta_box()
{
    $post_types = array('your_custom_post_type', 'page', 'post');
    foreach ($post_types as $post_type) {
        add_meta_box(
            'custom_meta_box', // $id
            'Additional Information', // $title
            'show_custom_meta_box', // $callback
             $post_type,
            'normal', // $context
            'high' // $priority
        );
    }
}
add_action('add_meta_boxes', 'add_custom_meta_box');
// The Callback
function show_custom_meta_box()
{
    global $custom_meta_fields, $post;

// Use nonce for verification
echo '<input type="hidden" name="custom_meta_box_nonce" value="'.wp_create_nonce(basename(__FILE__)).'" />';
// Begin the field table and loop
echo '<table class="form-table">';

    foreach ($custom_meta_fields as $field) {
        //Check if scope matches post type
$scope = $field[ 'scope' ];
        $field_output = false;
        foreach ($scope as $scopeItem) {
            switch ($scopeItem) {
default: {
if ($post->post_type == $scopeItem) {
    $field_output = true;
}
break;
}
}
            if ($field_output) {
                break;
            }
        }
        if ($field_output) {
            // get value of this field if it exists for this post
$meta = get_post_meta($post->ID, $field['id'], true);
            $row = 0;
// begin a table row with
echo '<tr>
<th><label for="'.$field['id'].'">'.$field['label'].'</label></th>
<td>';
            switch ($field['type']) {
// text
case 'text':
echo '<input type="text" name="'.$field['id'].'" id="'.$field['id'].'" value="'.$meta.'" size="30" />
<br /><span class="description">'.$field['desc'].'</span>';
break;
// repeatable
case 'repeatable_image':
 echo '<a class="repeatable-add button" href="#">+</a>
         <ul id="'.$field['id'].'-repeatable" class="custom_repeatable">';
 $i = 0;
 if ($meta) {
     foreach ($meta as $row) {
         echo '<li><span class="sort hndle">|||</span>
                     <input type="text" class="img_field" name="'.$field['id'].'['.$i.']" id="'.$field['id'].'" value="'.$row.'" size="30" />
                     <a class="repeatable-remove button" href="#">-</a></li>';
         ++$i;
     }
 } else {
     echo '<li><span class="sort hndle">|||</span>
                 <input class="img_field" type="text" name="'.$field['id'].'['.$i.']" id="'.$field['id'].'" value="" size="30" />
                 <a class="repeatable-remove button" href="#">-</a></li>';
 }
 echo '</ul>
     <span class="description">'.$field['desc'].'</span>';
break;
} //end switch
echo '</td></tr>';
        }
    } // end foreach
echo '</table>'; // end table
}

// Save the Data
function save_custom_meta($post_id)
{
    global $custom_meta_fields;
// verify nonce
if (!isset($_POST['custom_meta_box_nonce']) || !wp_verify_nonce($_POST['custom_meta_box_nonce'], basename(__FILE__))) {
    return $post_id;
}
// check autosave
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
    return $post_id;
}
// check permissions
if ('page' == $_POST['post_type']) {
    if (!current_user_can('edit_page', $post_id)) {
        return $post_id;
    } elseif (!current_user_can('edit_post', $post_id)) {
        return $post_id;
    }
}
// loop through fields and save the data
foreach ($custom_meta_fields as $field) {
    $old = get_post_meta($post_id, $field['id'], true);
    if (isset($_POST[$field['id']])) {
        $new = $_POST[$field['id']];
        if ($field['type'] === 'repeatable_ad' || $field['type'] === 'repeatable_image') {
            $new = array_values($new);
        }
    }
    if ($new && $new != $old) {
        update_post_meta($post_id, $field['id'], str_replace('"', "'", $new));
    } elseif ('' == $new && $old) {
        delete_post_meta($post_id, $field['id'], $old);
    }
} // end foreach
}
add_action('save_post', 'save_custom_meta');

and your js will need to be something like this

jQuery(document).on('click', '.img_field', function(e) {



var clicked_field = e.target.name;


var custom_uploader;


    e.preventDefault();

    //If the uploader object has already been created, reopen the dialog
    if (custom_uploader) {
        custom_uploader.open();
        return;
    }

    //Extend the wp.media object
    custom_uploader = wp.media.frames.file_frame = wp.media({
        title: 'Select Image',
        button: {
            text: 'Select Image'
        },
        multiple: false
    });

    //When a file is selected, grab the URL and set it as the text field's value
    custom_uploader.on('select', function() {
        attachment = custom_uploader.state().get('selection').first().toJSON();
        jQuery('input[name="'+ clicked_field +'"]').val(attachment.url);
        jQuery('.custom_preview_image').attr('src', attachment.url);
        jQuery('.custom_media_image').attr('src',attachment.url);
    });

    //Open the uploader dialog
    custom_uploader.open();

});


jQuery('.repeatable-add').click(function() {
    field = jQuery(this).closest('td').find('.custom_repeatable li:last').clone(true);
    fieldLocation = jQuery(this).closest('td').find('.custom_repeatable li:last');
    jQuery('input', field).val('').attr('name', function(index, name) {
        return name.replace(/(\d+)/, function(fullMatch, n) {
            return Number(n) + 1;
        });
    })
    field.insertAfter(fieldLocation, jQuery(this).closest('td'))
    return false;
});

jQuery('.repeatable-remove').click(function(){
    jQuery(this).parent().remove();
    return false;
});

jQuery('.custom_repeatable').sortable({
    opacity: 0.6,
    revert: true,
    cursor: 'move',
    handle: '.sort'
});

I haven’t tested this, so let me know if it doesn’t work but it should give you a good head start. Having said all that, it’s much easier to use ACF or CMB2 to do this sort of thing. Anyway, I hope this helps.